#
Write Bindings
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
.
typedef PyObject* (*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 callsize()
to get the length of the array. - The return value is a
PyObject*
, which should not benullptr
. If there is no return value, returnvm->None
.
#
Bind a function or method
Use vm->bind
to bind a function or method.
PyObject* bind(PyObject*, const char* sig, NativeFuncC)
PyObject* bind(PyObject*, const char* sig, const char* docstring, NativeFuncC)
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.
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:
- Captured lambda runs slower and causes "code-bloat".
- Captured values are unsafe, especially for
PyObject*
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.
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.
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
:
#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.
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.
int main(){
VM* vm = new VM();
PyObject* mod = vm->new_module("test");
vm->register_user_class<Point>(mod, "Point",
[](VM* vm, PyObject* mod, PyObject* 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 PyObject*
in its fields, you need to handle gc for them.
struct Container{
PyObject* a;
std::vector<PyObject*> b;
// ...
}
Add a magic method _gc_mark() const
to your custom type.
struct Container{
PyObject* a;
std::vector<PyObject*> b;
// ...
void _gc_mark() const{
// mark a
if(a) PK_OBJ_MARK(a);
// mark elements in b
for(PyObject* obj : b){
if(obj) PK_OBJ_MARK(obj);
}
}
}
For global objects, use the callback in vm->heap
.
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.
PyObject* f_add(VM* vm, PyObject* lhs, PyObject* 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 for an example used by random
module.
See 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 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.
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.
#
Example
#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;
}