mirror of
https://github.com/pocketpy/pocketpy
synced 2025-10-21 12:00:18 +00:00
commit 7e52f767ca130a49abb90ec922d74d2f5e9be078 Author: blueloveTH <blueloveTH@foxmail.com> Date: Sat Jun 1 12:49:46 2024 +0800 some optimize commit 3e2ad5b1fbad4367c80ea1325d1aa379282c10c4 Author: blueloveTH <blueloveTH@foxmail.com> Date: Sat Jun 1 12:29:53 2024 +0800 some fix commit bc0e530c72896a23cb6616ff4197ac36913389a4 Author: blueloveTH <blueloveTH@foxmail.com> Date: Sat Jun 1 00:00:47 2024 +0800 some fix commit f17ddcf8299c5d6803085cd3263181f44284f31b Author: blueloveTH <blueloveTH@foxmail.com> Date: Fri May 31 23:56:15 2024 +0800 some fix commit cc63926c8bb89df2d99d8c92c2e18bd5a0180a2c Author: blueloveTH <blueloveTH@foxmail.com> Date: Fri May 31 23:44:09 2024 +0800 some fix commit 3d3fb042651579cbdbcf3255398276ebb7b81e58 Author: blueloveTH <blueloveth@foxmail.com> Date: Fri May 31 17:28:13 2024 +0800 deprecate `PK_OBJ_MARK` commit 3df5f1cf128f157fb3a7aac2ceeeb47c55f5bb3b Author: blueloveTH <blueloveth@foxmail.com> Date: Fri May 31 17:18:34 2024 +0800 init
306 lines
8.6 KiB
Markdown
306 lines
8.6 KiB
Markdown
---
|
|
icon: cpu
|
|
title: Write Bindings
|
|
order: 18
|
|
---
|
|
|
|
In order to use a C/C++ library in python, you need to write bindings for it.
|
|
|
|
pkpy uses an universal signature to wrap a function pointer as a python function or method that can be called in python code, i.e `NativeFuncC`.
|
|
|
|
```cpp
|
|
typedef PyVar (*NativeFuncC)(VM*, ArgsView);
|
|
```
|
|
+ The first argument is the pointer of `VM` instance.
|
|
+ The second argument is an array-like object indicates the arguments list. You can use `[]` operator to get the element and call `size()` to get the length of the array.
|
|
+ The return value is a `PyVar`, which should not be `nullptr`. If there is no return value, return `vm->None`.
|
|
|
|
## Bind a function or method
|
|
|
|
Use `vm->bind` to bind a function or method.
|
|
|
|
+ `PyVar bind(PyVar, const char* sig, NativeFuncC)`
|
|
+ `PyVar bind(PyVar, const char* sig, const char* docstring, NativeFuncC)`
|
|
|
|
```cpp
|
|
|
|
vm->bind(obj, "add(a: int, b: int) -> int", [](VM* vm, ArgsView args){
|
|
int a = py_cast<int>(vm, args[0]);
|
|
int b = py_cast<int>(vm, args[1]);
|
|
return py_var(vm, a + b);
|
|
});
|
|
|
|
// or you can provide a docstring
|
|
vm->bind(obj,
|
|
"add(a: int, b: int) -> int",
|
|
"add two integers", [](VM* vm, ArgsView args){
|
|
int a = py_cast<int>(vm, args[0]);
|
|
int b = py_cast<int>(vm, args[1]);
|
|
return py_var(vm, a + b);
|
|
});
|
|
```
|
|
|
|
### How to capture something
|
|
|
|
By default, the lambda being bound is a C function pointer,
|
|
you cannot capture anything! The following example does not compile.
|
|
|
|
```cpp
|
|
int x = 1;
|
|
vm->bind(obj, "f() -> int", [x](VM* vm, ArgsView args){
|
|
// error: cannot capture 'x'
|
|
return py_var(vm, x);
|
|
});
|
|
```
|
|
|
|
I do not encourage you to capture something in a lambda being bound
|
|
because:
|
|
1. Captured lambda runs slower and causes "code-bloat".
|
|
2. Captured values are unsafe, especially for `PyVar` as they could leak by accident.
|
|
|
|
However, there are 3 ways to capture something when you really need to.
|
|
The most safe and elegant way is to subclass `VM` and add a member variable.
|
|
|
|
```cpp
|
|
class YourVM : public VM{
|
|
public:
|
|
int x;
|
|
YourVM() : VM() {}
|
|
};
|
|
|
|
int main(){
|
|
YourVM* vm = new YourVM();
|
|
vm->x = 1;
|
|
vm->bind(obj, "f() -> int", [](VM* _vm, ArgsView args){
|
|
// do a static_cast and you can get any extra members of YourVM
|
|
YourVM* vm = static_cast<YourVM*>(_vm);
|
|
return py_var(vm, vm->x);
|
|
});
|
|
return 0;
|
|
}
|
|
```
|
|
|
|
The 2nd way is to use `vm->bind`'s last parameter `userdata`, you can store an `pkpy::any` object.
|
|
And use `lambda_get_userdata<T>(args.begin())` to get it inside the lambda body.
|
|
|
|
```cpp
|
|
int x = 1;
|
|
vm->bind(obj, "f() -> int", [](VM* vm, ArgsView args){
|
|
// get the userdata
|
|
int x = lambda_get_userdata<int>(args.begin());
|
|
return py_var(vm, x);
|
|
}, x); // capture x
|
|
```
|
|
|
|
The 3rd way is to change the macro `PK_ENABLE_STD_FUNCTION` in `config.h`:
|
|
```cpp
|
|
#define PK_ENABLE_STD_FUNCTION 0 // => 1
|
|
```
|
|
|
|
Then you can use standard capture list in lambda.
|
|
|
|
## Bind a class or struct
|
|
|
|
Assume you have a struct `Point` declared as follows.
|
|
|
|
```cpp
|
|
struct Point{
|
|
int x;
|
|
int y;
|
|
}
|
|
```
|
|
|
|
You can create a `test` module and use `vm->register_user_class<>` to bind the class to the test module.
|
|
|
|
```cpp
|
|
int main(){
|
|
VM* vm = new VM();
|
|
PyObject* mod = vm->new_module("test");
|
|
vm->register_user_class<Point>(mod, "Point",
|
|
[](VM* vm, PyVar mod, PyVar type){
|
|
// wrap field x
|
|
vm->bind_field(type, "x", &Point::x);
|
|
// wrap field y
|
|
vm->bind_field(type, "y", &Point::y);
|
|
|
|
// __init__ method
|
|
vm->bind(type, "__init__(self, x, y)", [](VM* vm, ArgsView args){
|
|
Point& self = _py_cast<Point&>(vm, args[0]);
|
|
self.x = py_cast<int>(vm, args[1]);
|
|
self.y = py_cast<int>(vm, args[2]);
|
|
return vm->None;
|
|
});
|
|
});
|
|
|
|
// use the Point class
|
|
vm->exec("import test");
|
|
vm->exec("a = test.Point(1, 2)");
|
|
vm->exec("print(a.x)"); // 1
|
|
vm->exec("print(a.y)"); // 2
|
|
|
|
delete vm;
|
|
return 0;
|
|
}
|
|
```
|
|
|
|
### Handle gc for container types
|
|
|
|
If your custom type stores `PyVar` in its fields, you need to handle gc for them.
|
|
|
|
```cpp
|
|
struct Container{
|
|
PyVar a;
|
|
std::vector<PyVar> b;
|
|
// ...
|
|
}
|
|
```
|
|
|
|
Add a magic method `_gc_mark(VM*) const` to your custom type.
|
|
|
|
```cpp
|
|
struct Container{
|
|
PyVar a;
|
|
std::vector<PyVar> b;
|
|
// ...
|
|
|
|
void _gc_mark(VM* vm) const{
|
|
// mark a
|
|
vm->obj_gc_mark(a);
|
|
|
|
// mark elements in b
|
|
for(PyVar obj : b){
|
|
vm->obj_gc_mark(obj);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
For global objects, use the callback in `vm->heap`.
|
|
```cpp
|
|
void (*_gc_marker_ex)(VM*) = nullptr;
|
|
```
|
|
It will be invoked before a GC starts. So you can mark objects inside the callback to keep them alive.
|
|
|
|
## Others
|
|
|
|
For some magic methods, we provide specialized binding function.
|
|
They do not take universal function pointer as argument.
|
|
You need to provide the detailed `Type` object and the corresponding function pointer.
|
|
|
|
```cpp
|
|
PyVar f_add(VM* vm, PyVar lhs, PyVar rhs){
|
|
int a = py_cast<int>(vm, lhs);
|
|
int b = py_cast<int>(vm, rhs);
|
|
return py_var(vm, a + b);
|
|
}
|
|
|
|
vm->bind__add__(vm->tp_int, f_add);
|
|
```
|
|
|
|
This specialized binding function has optimizations and result in better performance when calling from python code.
|
|
|
|
For example, `vm->bind__add__` is preferred over `vm->bind_func(type, "__add__", 2, f_add)`.
|
|
|
|
|
|
## Further reading
|
|
|
|
See [random.cpp](https://github.com/pocketpy/pocketpy/blob/main/src/random.cpp) for an example used by `random` module.
|
|
|
|
See [collections.cpp](https://github.com/pocketpy/pocketpy/blob/main/src/collections.cpp) for a modern implementation of `collections.deque`.
|
|
|
|
## Reuse Lua bindings
|
|
|
|
pkpy provides a lua bridge to reuse lua bindings.
|
|
It allows you to run lua code and call lua functions in python
|
|
by embedding a lua virtual machine.
|
|
|
|
Add `lua_bridge.hpp` and `lua_bridge.cpp` in [3rd/lua_bridge](https://github.com/pocketpy/pocketpy/tree/main/3rd/lua_bridge) to your project.
|
|
Make sure `lua.h`, `lualib.h` and `lauxlib.h` are in your include path
|
|
because `lua_bridge.hpp` needs them.
|
|
|
|
The lua bridge is based on lua 5.1.5 for maximum compatibility.
|
|
lua 5.2 or higher should also work.
|
|
|
|
### Setup
|
|
|
|
Use `initialize_lua_bridge(VM*, lua_State*)` to initialize the lua bridge.
|
|
This creates a new module `lua` in your python virtual machine.
|
|
|
|
You can use `lua.dostring` to execute lua code and get the result.
|
|
And use `lua.Table()` to create a lua table.
|
|
A `lua.Table` instance in python is a dict-like object which provides a bunch of
|
|
magic methods to access the underlying lua table.
|
|
|
|
```python
|
|
class Table:
|
|
def keys(self) -> list:
|
|
"""Return a list of keys in the table."""
|
|
|
|
def values(self) -> list:
|
|
"""Return a list of values in the table."""
|
|
|
|
def items(self) -> list[tuple]:
|
|
"""Return a list of (key, value) pairs in the table."""
|
|
|
|
def __len__(self) -> int:
|
|
"""Return the length of the table."""
|
|
|
|
def __contains__(self, key) -> bool:
|
|
"""Return True if the table contains the key."""
|
|
|
|
def __getitem__(self, key): ...
|
|
def __setitem__(self, key, value): ...
|
|
def __delitem__(self, key): ...
|
|
def __getattr__(self, key): ...
|
|
def __setattr__(self, key, value): ...
|
|
def __delattr__(self, key): ...
|
|
```
|
|
|
|
Only basic types can be passed between python and lua.
|
|
The following table shows the type mapping.
|
|
If you pass an unsupported type, an exception will be raised.
|
|
|
|
| Python type | Lua type | Allow create in Python? | Reference? |
|
|
| ----------- | -------- | ---------------------- | --------- |
|
|
| `None` | `nil` | YES | NO |
|
|
| `bool` | `boolean` | YES | NO |
|
|
| `int` | `number` | YES | NO |
|
|
| `float` | `number` | YES | NO |
|
|
| `str` | `string` | YES | NO |
|
|
| `tuple` | `table` | YES | NO |
|
|
| `list` | `table` | YES | NO |
|
|
| `dict` | `table` | YES | NO |
|
|
| `lua.Table` | `table` | YES | YES |
|
|
| `lua.Function`| `function`| NO | YES |
|
|
|
|
### Example
|
|
```cpp
|
|
#include "lua_bridge.hpp"
|
|
|
|
using namespace pkpy;
|
|
|
|
int main(){
|
|
VM* vm = new VM();
|
|
|
|
// create lua state
|
|
lua_State* L = luaL_newstate();
|
|
luaL_openlibs(L);
|
|
|
|
// initialize lua bridge
|
|
initialize_lua_bridge(vm, L);
|
|
|
|
// dostring to get _G
|
|
vm->exec("import lua");
|
|
vm->exec("g = lua.dostring('return _G')");
|
|
|
|
// create a table
|
|
vm->exec("t = lua.Table()");
|
|
vm->exec("t.a = 1");
|
|
vm->exec("t.b = 2");
|
|
|
|
// call lua function
|
|
vm->exec("g.print(t.a + t.b)"); // 3
|
|
|
|
return 0;
|
|
}
|
|
``` |