8.2 KiB
icon | title | order |
---|---|---|
cpu | Write C Bindings | 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 C function pointer as a python function or method, i.e py_CFunction
.
typedef bool (*py_CFunction)(int argc, py_Ref argv);
argc
is the number of arguments passed to the function.argv
is the pointer to the first argument.
If successful, the function should return true
and set the return value in py_retval()
. In case there is no return value, you should use py_newnone(py_retval())
.
If an error occurs, the function should raise an exception and return false
.
Steps
Bind a simple function
Say you have a function add
that takes two integers and returns their sum.
int add(int a, int b) {
return a + b;
}
Here is how you can write the binding for it:
// 1. Define a wrapper function with the signature `py_CFunction`.
bool py_add(int argc, py_Ref argv) {
// 2. Check the number of arguments.
PY_CHECK_ARGC(2);
// 3. Check the type of arguments.
PY_CHECK_ARG_TYPE(0, tp_int);
PY_CHECK_ARG_TYPE(1, tp_int);
// 4. Convert the arguments into C types.
int _0 = py_toint(py_arg(0));
int _1 = py_toint(py_arg(1));
// 5. Call the original function.
int res = add(_0, _1);
// 6. Set the return value.
py_newint(py_retval(), res);
// 7. Return `true`.
return true;
}
Once you have the wrapper function, you can bind it to a python module via py_bindfunc
.
py_GlobalRef mod = py_getmodule("__main__");
py_bindfunc(mod, "add", py_add);
Alternatively, you can use py_bind
with a signature, which allows you to specify some default values.
py_GlobalRef mod = py_getmodule("__main__");
py_bind(mod, "add(a, b=1)", py_add);
Bind a struct
If you have a struct like this:
typedef struct MyStruct{
int x;
int datasize;
int* data;
}MyStruct;
x
is some kind of property of the struct, and this struct is used for store datasize
numbers.
Here's how you can create a MyStruct
:
// 1. Define a wrapper function with the signature `py_CFunction`.
bool MyStruct__new__(int argc, py_Ref argv) {
// 2. Check the number of arguments.
PY_CHECK_ARGC(3);
// 3. Check the type of arguments.
PY_CHECK_ARG_TYPE(0, tp_type);
PY_CHECK_ARG_TYPE(1, tp_int);
PY_CHECK_ARG_TYPE(2, tp_int);
// 4. Convert the arguments into C types.
py_Type cls = py_totype(py_arg(0));
int x = py_toint(py_arg(1));
int datasize = py_toint(py_arg(2));
// 5. Create a MyStruct instance, where `datasize` gives correspond slots to store numbers.
MyStruct* res = py_newobject(py_pushtmp(), cls, datasize, sizeof(MyStruct));
// 6. Set the values.
res->x = x;
res->datasize = datasize;
// 7. `data` is in the head of slots, init `data` with zeros.
res->data = py_getslot(py_peek(-1), 0);
for (int i = 0; i < datasize; i++) {
res->data[i] = 0;
}
// 8. Put the created struct into the return value register.
py_assign(py_retval(), py_peek(-1));
// 9. Pop the struct safely.
py_pop();
return true;
}
Function for getting the property x
from MyStruct
:
bool MyStruct_x(int argc, py_Ref argv) {
// 1. Check the number of arguments.
PY_CHECK_ARGC(1);
// 2. Convert the arguments into C types.
MyStruct* self = py_touserdata(argv);
// 3. Set the x value.
py_newint(py_retval(), self->x);
// 4. Return `true`.
return true;
}
Function for getting a specified number from data
:
bool MyStruct_data_get(int argc, py_Ref argv) {
// 1. Check the number and type of arguments.
PY_CHECK_ARGC(2);
PY_CHECK_ARG_TYPE(1, tp_int);
// 2. Convert the arguments into C types.
MyStruct* self = py_touserdata(argv);
int index = py_toint(py_arg(1));
// 3. Exception if the index is out of range.
if (index >= self->datasize) {
IndexError("Not a valid index");
}
// 4. Return the value.
py_newint(py_retval(), self->data[index]);
// 5. Return `true`.
return true;
}
Function for setting a number's value in data
:
bool MyStruct_data_set(int argc, py_Ref argv) {
// 1. Check the number and type of arguments.
PY_CHECK_ARGC(3);
PY_CHECK_ARG_TYPE(1, tp_int);
PY_CHECK_ARG_TYPE(2, tp_int);
// 2. Convert the arguments into C types.
MyStruct* self = py_touserdata(argv);
int index = py_toint(py_arg(1));
int value = py_toint(py_arg(2));
// 3. Exception if the index is out of range.
if (index >= self->datasize) {
IndexError("Not a valid index");
}
// 4. Set the value.
self->data[index] = value;
// 5. All functions should have a return value. None is returned here.
py_newnone(py_retval());
// 6. Return `true`.
return true;
}
Now you can bind the functions to the new module mmystruct
:
py_GlobalRef mod = py_newmodule("mystruct");
// 1. Add a custom type.
py_Type mystruct = py_newtype("custom_struct", tp_object, mod, NULL);
// 2. Bind the function of creating MyStruct.
py_bind(py_tpobject(mystruct), "__new__(cls, x: int, datasize: int)", MyStruct__new__);
// 3. Bind the property `x`.
py_bindproperty(mystruct, "x", MyStruct_x, NULL);
// 4. Bind magic methods of operating numbers in `data`.
py_bindmagic(mystruct, __getitem__, MyStruct_data_get);
py_bindmagic(mystruct, __setitem__, MyStruct_data_set);
You can use it like this:
import mystruct
test = mystruct.custom_struct(3,4) # x=3, 4 slots for data
print(test.x)
print(test[1]) # 0
test[1] = 100
print(test[1]) # 100
Bind a function with arbitrary argument lists
Sometimes you want a function that takes arbitrary input arguments. For example, sum several numbers in the table,
or make a simple print
function.
Sum several numbers
Say you have 2,3,4,5,6 and put them into the sum
function. Here's an implementation:
bool py_sum(int argc, py_Ref argv) {
// 1. These numbers are packed as a tuple
PY_CHECK_ARG_TYPE(0, tp_tuple);
// 2. Get the length of the tuple
int len = py_tuple_len(py_arg(0));
int res = 0;
// 3. Sum the numbers up.
for (int i = 0; i < len; i++) {
int _0 = py_toint(py_tuple_getitem(py_arg(0), i));
res += _0;
}
// 4. Set the result.
py_newint(py_retval(), res);
// 5. Return `true`.
return res;
}
And then bind it:
py_GlobalRef mod = py_newmodule("sumary");
py_bind(mod, "sum(*values: tuple[int])", py_sum);
It can be used like this:
import sumary
print(sumary.sum(2,3,4,5,6))
Make a simple print function
Let's make a simple print function now. It takes arbitrary argument *values
, and end
/sep
is not necessary. It's so simple that only string argument is acceptable.
Here's an implementation:
bool py_print(int argc, py_Ref argv) {
// 1. *values is always a tuple.
PY_CHECK_ARG_TYPE(0, tp_tuple);
// 2. Get the length of tuple.
int len = py_tuple_len(py_arg(0));
const char* end = "\n";
const char* sep = " ";
// 3. First arg is sep, but it could be None.
if (!py_isnone(py_arg(1))) {
PY_CHECK_ARG_TYPE(1, tp_str);
sep = py_tostr(py_arg(1));
}
// 4. Second arg is end, it also can be None.
if (!py_isnone(py_arg(2))) {
PY_CHECK_ARG_TYPE(2, tp_str);
end = py_tostr(py_arg(2));
}
// 5. Print.
for (int i = 0; i < len; i++) {
if (i > 0) {
printf("%s", sep);
}
// 6. It can print iterable like `list` if you modify this line.
printf("%s", py_tostr(py_tuple_getitem(py_arg(0), i)));
}
printf("%s", end);
// 7. All the functions should return a value, here None is returned.
py_newnone(py_retval());
return true;
}
And then bind:
py_GlobalRef mod = py_newmodule("myprint");
py_bind(mod, "my_print(*values: object, sep: str | None = None, end: str | None = None)", py_print);
It can print names like this:
import myprint
myprint.my_print('Bob','Mary', end = 'Cake', sep = '|')
See also: