# HG changeset patch # User Tassilo Philipp # Date 1612294922 -3600 # Node ID 918dab7a6606281deaac654f586731f02c42fc47 # Parent 6387d39ecce2d0a9594efd9dd44d8d8863bdd304 - added callback support (comes with some bigger refactoring) - allow CPython's Py{CObject,Capsule} to be used as 'p'ointers diff -r 6387d39ecce2 -r 918dab7a6606 python/pydc/README.txt --- a/python/pydc/README.txt Fri Jan 22 15:18:56 2021 +0100 +++ b/python/pydc/README.txt Tue Feb 02 20:42:02 2021 +0100 @@ -15,6 +15,8 @@ Nov 13, 2020: removed pydc.py wrapper overhead (which only called pydcext.so functions; implies renaming pydcext.* to pydc.*), added type stub as package_data +Feb 2, 2021: added callback support (comes with some bigger refactoring); + allow CPython's Py{CObject,Capsule} to be used as 'p'ointers BUILD/INSTALLATION @@ -33,13 +35,18 @@ API === -In a nutshell: +In a nutshell for all calls: libhandle = load(libpath) # if path == None => handle to running process libpath = get_path(libhandle) # if handle == None => path to executable -funcptr = find(libhandle, symbolname) -call(funcptr, signature, ...) -free(libhandle) +funcptr = find(libhandle, symbolname) # lookup symbol by name +call(funcptr, signature, ...) # call C func w/ signature and corresponding args +free(libhandle) # free library + +For callback objects to be passed as 'p'ointer args: + +cbhandle = new_callback(signature, pyfunc) # signature reflecting C func ptr +free_callback(cbhandle) # release callback object Notes: - a pydc.pyi stub file with the precise interface description is available @@ -84,6 +91,7 @@ | int (PyInt) | int (PyLong) | void* | int,long (Py_ssize_t) | int (Py_ssize_t) | long (PyLong) | - | void* | int,long (Py_ssize_t) | int (Py_ssize_t) | None (Py_None) | None (Py_None) | void* (always NULL) | int,long (Py_ssize_t) | int (Py_ssize_t) + | (PyCObject,PyCapsule) | (PyCObject,PyCapsule) | void* | int,long (Py_ssize_t) | int (Py_ssize_t) @@@ test 'Z' | str (PyString) ! | str (PyUnicode) ! | const char* (UTF-8 for unicode) | str (PyString) | str (PyUnicode) | unicode (PyUnicode) ! | - | const char* (UTF-8 for unicode) | str (PyString) | str (PyUnicode) | - | bytes (PyBytes) ! | const char* (UTF-8 for unicode) | str (PyString) | str (PyUnicode) @@ -96,7 +104,7 @@ $ cast to single precision ^ cast to double precision & mutable buffer when passed to C - ! immutable buffer when passed to C, as strings (in any form) are considered objects, not buffers + ! immutable buffer when passed to C, as strings (in any form) are considered objects, not buffers; also, not allowed as return type in callback signatures Also supported are specifying calling convention switches using '_'-prefixed @@ -122,7 +130,7 @@ TODO ==== -- callback support +- calling convention mode handling for callbacks (not sure if ever needed?) - pydoc "man page" - stub location: the pydc-stubs folder isn't picked up by mypy, so I question why this is the suggested way - get into freebsd ports diff -r 6387d39ecce2 -r 918dab7a6606 python/pydc/examples/callback.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/python/pydc/examples/callback.py Tue Feb 02 20:42:02 2021 +0100 @@ -0,0 +1,58 @@ +# callback of python function to qsort(3) some numbers - this is just a example +# using an existing libc function that uses a callback; it's not practical for +# real world use as it comes with a huge overhead: +# - sorting requires many calls of the comparison function +# - each such callback back into python comes with a lot of overhead +# - on top of that, for this example, 2 memcpy(3)s are needed to access the +# data to compare, further adding to the overhead + +from pydc import * +import sys +import platform +import struct + +if sys.platform == "win32": + libc = load("msvcrt") +elif sys.platform == "darwin": + libc = load("/usr/lib/libc.dylib") +elif "bsd" in sys.platform: + #libc = load("/usr/lib/libc.so") + libc = load("/lib/libc.so.7") +elif platform.architecture()[0] == "64bit": + libc = load("/lib64/libc.so.6") +else: + libc = load("/lib/libc.so.6") + + + +fp_qsort = find(libc,"qsort") # void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *)); +fp_memcpy = find(libc,"memcpy") # void * memcpy(void *dst, const void *src, size_t len); + + + +nums = bytearray(struct.pack("i"*8, 12, 3, 5, 99, 3, -9, -9, 0)) +es = int(len(nums)/8) # element size + + +def compar(a, b): + ba = bytearray(es) + call(fp_memcpy,"ppi)p", ba, a, es) + a = struct.unpack("i", ba)[0] + call(fp_memcpy,"ppi)p", ba, b, es) + b = struct.unpack("i", ba)[0] + return a - b + +cb = new_callback("pp)i", compar) + +# -------- + +print(*struct.unpack("i"*8, nums)) + +print('... qsort ...') +call(fp_qsort,"piip)v", nums, 8, es, cb) + +print(*struct.unpack("i"*8, nums)) + + +free_callback(cb) + diff -r 6387d39ecce2 -r 918dab7a6606 python/pydc/pydc.c --- a/python/pydc/pydc.c Fri Jan 22 15:18:56 2021 +0100 +++ b/python/pydc/pydc.c Tue Feb 02 20:42:02 2021 +0100 @@ -13,6 +13,7 @@ #include #include "dynload.h" #include +#include @@ -22,11 +23,13 @@ # define DcPyCObject_FromVoidPtr(ptr, dtor) PyCObject_FromVoidPtr((ptr), (dtor)) // !new ref! # define DcPyCObject_AsVoidPtr(ppobj) PyCObject_AsVoidPtr((ppobj)) # define DcPyCObject_SetVoidPtr(ppobj, ptr) PyCObject_SetVoidPtr((ppobj), (ptr)) +# define DcPyCObject_Check(ppobj) PyCObject_Check((ppobj)) #else # define USE_CAPSULE_API # define DcPyCObject_FromVoidPtr(ptr, dtor) PyCapsule_New((ptr), NULL, (dtor)) // !new ref! # define DcPyCObject_AsVoidPtr(ppobj) PyCapsule_GetPointer((ppobj), NULL) # define DcPyCObject_SetVoidPtr(ppobj, ptr) //@@@ unsure what to do, cannot/shouldn't call this with a null pointer as this wants to call the dtor, so not doing anything: PyCapsule_SetPointer((ppobj), (ptr)) // this might need to call the dtor to behave like PyCObject_SetVoidPtr? +# define DcPyCObject_Check(ppobj) PyCapsule_CheckExact((ppobj)) #endif #if(PY_VERSION_HEX >= 0x03030000) @@ -54,11 +57,11 @@ /* PyCObject destructor callback for libhandle */ #if defined(USE_CAPSULE_API) -void free_library(PyObject* capsule) +static void free_library(PyObject* capsule) { void* libhandle = PyCapsule_GetPointer(capsule, NULL); #else -void free_library(void* libhandle) +static void free_library(void* libhandle) { #endif if (libhandle != 0) @@ -167,6 +170,119 @@ #include "dyncall.h" #include "dyncall_signature.h" + +/* helpers */ + +static inline PyObject* py2dcchar(DCchar* c, PyObject* po, int u, int pos) +{ + if ( PyUnicode_Check(po) ) + { +#if (PY_VERSION_HEX < 0x03030000) + Py_UNICODE cu; + if (PyUnicode_GET_SIZE(po) != 1) +#else + Py_UCS4 cu; + if (PyUnicode_GET_LENGTH(po) != 1) +#endif + return PyErr_Format( PyExc_RuntimeError, "arg %d - expecting a str with length of 1 (a char string)", pos ); + +#if (PY_VERSION_HEX < 0x03030000) + cu = PyUnicode_AS_UNICODE(po)[0]; +#else + cu = PyUnicode_ReadChar(po, 0); +#endif + // check against UCHAR_MAX in every case b/c Py_UCS4 is unsigned + if ( (cu > UCHAR_MAX)) + return PyErr_Format( PyExc_RuntimeError, "arg %d out of range - expecting a char code", pos ); + *c = (DCchar) cu; + return po; + } + + if ( DcPyString_Check(po) ) + { + size_t l; + char* s; + l = DcPyString_GET_SIZE(po); + if (l != 1) + return PyErr_Format( PyExc_RuntimeError, "arg %d - expecting a str with length of 1 (a char string)", pos ); + s = DcPyString_AsString(po); + *c = (DCchar) s[0]; + return po; + } + + if ( DcPyInt_Check(po) ) + { + long l = DcPyInt_AsLong(po); + if (u && (l < 0 || l > UCHAR_MAX)) + return PyErr_Format( PyExc_RuntimeError, "arg %d out of range - expecting 0 <= arg <= %d, got %ld", pos, UCHAR_MAX, l ); + if (!u && (l < CHAR_MIN || l > CHAR_MAX)) + return PyErr_Format( PyExc_RuntimeError, "arg %d out of range - expecting %d <= arg <= %d, got %ld", pos, CHAR_MIN, CHAR_MAX, l ); + *c = (DCchar) l; + return po; + } + + return PyErr_Format( PyExc_RuntimeError, "arg %d - expecting a char", pos ); +} + +static inline PyObject* py2dcshort(DCshort* s, PyObject* po, int u, int pos) +{ + long l; + if ( !DcPyInt_Check(po) ) + return PyErr_Format( PyExc_RuntimeError, "arg %d - expecting an int", pos ); + l = DcPyInt_AS_LONG(po); + if (u && (l < 0 || l > USHRT_MAX)) + return PyErr_Format( PyExc_RuntimeError, "arg %d out of range - expecting 0 <= arg <= %d, got %ld", pos, USHRT_MAX, l ); + if (!u && (l < SHRT_MIN || l > SHRT_MAX)) + return PyErr_Format( PyExc_RuntimeError, "arg %d out of range - expecting %d <= arg <= %d, got %ld", pos, SHRT_MIN, SHRT_MAX, l ); + + *s = (DCshort)l; + return po; +} + +static inline PyObject* py2dclonglong(DClonglong* ll, PyObject* po, int pos) +{ +#if PY_MAJOR_VERSION < 3 + if ( PyInt_Check(po) ) { + *ll = (DClonglong) PyInt_AS_LONG(po); + return po; + } +#endif + if ( !PyLong_Check(po) ) + return PyErr_Format( PyExc_RuntimeError, "arg %d - expecting " EXPECT_LONG_TYPE_STR, pos ); + + *ll = (DClonglong) PyLong_AsLongLong(po); + return po; +} + +static inline PyObject* py2dcpointer(DCpointer* p, PyObject* po, int pos) +{ + if ( PyByteArray_Check(po) ) { + *p = (DCpointer) PyByteArray_AsString(po); // adds an extra '\0', but that's ok + return po; + } +#if PY_MAJOR_VERSION < 3 + if ( PyInt_Check(po) ) { + *p = (DCpointer) PyInt_AS_LONG(po); + return po; + } +#endif + if ( PyLong_Check(po) ) { + *p = (DCpointer) PyLong_AsVoidPtr(po); + return po; + } + if ( po == Py_None ) { + *p = NULL; + return po; + } + if ( DcPyCObject_Check(po) ) { + *p = DcPyCObject_AsVoidPtr(po); + return po; + } + + return PyErr_Format( PyExc_RuntimeError, "arg %d - expecting a promoting pointer-type (int), mutable array (bytearray) or callback func handle (int, created with new_callback())", pos ); +} + + DCCallVM* gpCall = NULL; // helper to temporarily copy string arguments @@ -256,73 +372,19 @@ case DC_SIGCHAR_UCHAR: { DCchar c; - if ( PyUnicode_Check(po) ) - { -#if (PY_VERSION_HEX < 0x03030000) - Py_UNICODE cu; - if (PyUnicode_GET_SIZE(po) != 1) -#else - Py_UCS4 cu; - if (PyUnicode_GET_LENGTH(po) != 1) -#endif - return PyErr_Format( PyExc_RuntimeError, "arg %d - expecting a str with length of 1 (a char string)", pos ); - -#if (PY_VERSION_HEX < 0x03030000) - cu = PyUnicode_AS_UNICODE(po)[0]; -#else - cu = PyUnicode_ReadChar(po, 0); -#endif - // check against UCHAR_MAX in every case b/c Py_UCS4 is unsigned - if ( (cu > UCHAR_MAX)) - return PyErr_Format( PyExc_RuntimeError, "arg %d out of range - expecting a char code", pos ); - c = (DCchar) cu; - } - else if ( DcPyString_Check(po) ) - { - size_t l; - char* s; - l = DcPyString_GET_SIZE(po); - if (l != 1) - return PyErr_Format( PyExc_RuntimeError, "arg %d - expecting a str with length of 1 (a char string)", pos ); - s = DcPyString_AsString(po); - c = (DCchar) s[0]; - } - else if ( DcPyInt_Check(po) ) - { - long l = DcPyInt_AsLong(po); - if (ch == DC_SIGCHAR_CHAR && (l < CHAR_MIN || l > CHAR_MAX)) - return PyErr_Format( PyExc_RuntimeError, "arg %d out of range - expecting %d <= arg <= %d, got %ld", pos, CHAR_MIN, CHAR_MAX, l ); - if (ch == DC_SIGCHAR_UCHAR && (l < 0 || l > UCHAR_MAX)) - return PyErr_Format( PyExc_RuntimeError, "arg %d out of range - expecting 0 <= arg <= %d, got %ld", pos, UCHAR_MAX, l ); - c = (DCchar) l; - } - else - return PyErr_Format( PyExc_RuntimeError, "arg %d - expecting a char", pos ); + if(!py2dcchar(&c, po, ch == DC_SIGCHAR_UCHAR, pos)) + return NULL; dcArgChar(gpCall, c); } break; case DC_SIGCHAR_SHORT: - { - long l; - if ( !DcPyInt_Check(po) ) - return PyErr_Format( PyExc_RuntimeError, "arg %d - expecting an int", pos ); - l = DcPyInt_AS_LONG(po); - if (l < SHRT_MIN || l > SHRT_MAX) - return PyErr_Format( PyExc_RuntimeError, "arg %d out of range - expecting %d <= arg <= %d, got %ld", pos, SHRT_MIN, SHRT_MAX, l ); - dcArgShort(gpCall, (DCshort)l); - } - break; - case DC_SIGCHAR_USHORT: { - long l; - if ( !DcPyInt_Check(po) ) - return PyErr_Format( PyExc_RuntimeError, "arg %d - expecting an int", pos ); - l = DcPyInt_AS_LONG(po); - if (l < 0 || l > USHRT_MAX) - return PyErr_Format( PyExc_RuntimeError, "arg %d out of range - expecting 0 <= arg <= %d, got %ld", pos, USHRT_MAX, l ); - dcArgShort(gpCall, (DCshort)l); + DCshort s; + if(!py2dcshort(&s, po, ch == DC_SIGCHAR_USHORT, pos)) + return NULL; + dcArgShort(gpCall, s); } break; @@ -342,14 +404,12 @@ case DC_SIGCHAR_LONGLONG: case DC_SIGCHAR_ULONGLONG: -#if PY_MAJOR_VERSION < 3 - if ( PyInt_Check(po) ) - dcArgLongLong(gpCall, (DClonglong) PyInt_AS_LONG(po)); - else -#endif - if ( !PyLong_Check(po) ) - return PyErr_Format( PyExc_RuntimeError, "arg %d - expecting " EXPECT_LONG_TYPE_STR, pos ); - dcArgLongLong(gpCall, (DClonglong)PyLong_AsLongLong(po)); + { + DClonglong ll; + if(!py2dclonglong(&ll, po, pos)) + return NULL; + dcArgLongLong(gpCall, ll); + } break; case DC_SIGCHAR_FLOAT: @@ -364,24 +424,14 @@ dcArgDouble(gpCall, PyFloat_AsDouble(po)); break; - case DC_SIGCHAR_POINTER: // this will only accept integers or mutable array types (meaning only bytearray) - { - DCpointer p; - if ( PyByteArray_Check(po) ) - p = (DCpointer) PyByteArray_AsString(po); // adds an extra '\0', but that's ok -#if PY_MAJOR_VERSION < 3 - else if ( PyInt_Check(po) ) - p = (DCpointer) PyInt_AS_LONG(po); -#endif - else if ( PyLong_Check(po) ) - p = (DCpointer) PyLong_AsVoidPtr(po); - else if ( po == Py_None ) - p = NULL; - else - return PyErr_Format( PyExc_RuntimeError, "arg %d - expecting a promoting pointer-type (int, bytearray)", pos ); - dcArgPointer(gpCall, p); - } - break; + case DC_SIGCHAR_POINTER: // this will only accept integers, mutable array types (meaning only bytearray) or tuples describing a callback + { + DCpointer p; + if(!py2dcpointer(&p, po, pos)) + return NULL; + dcArgPointer(gpCall, p); + } + break; case DC_SIGCHAR_STRING: // strings are considered to be immutable objects { @@ -390,9 +440,6 @@ size_t s; if ( PyUnicode_Check(po) ) { - if(n_str_aux >= NUM_AUX_STRS) - return PyErr_Format( PyExc_RuntimeError, "too many arguments (implementation limit of %d new UTF-8 string references reached) - abort", n_str_aux ); - #if defined(PYUNICODE_CACHES_UTF8) p = PyUnicode_AsUTF8(po); #else @@ -407,7 +454,10 @@ else return PyErr_Format( PyExc_RuntimeError, "arg %d - expecting a str", pos ); - // p points in any case to a buffer that shouldn't be modified, so pass a copy to dyncall (cleaned up after call) + if(n_str_aux >= NUM_AUX_STRS) + return PyErr_Format( PyExc_RuntimeError, "too many arguments (implementation limit of %d new UTF-8 string references reached) - abort", n_str_aux ); + + // p points in every case to a buffer that shouldn't be modified, so pass a copy to dyncall (cleaned up after call) s = strlen(p)+1; str_aux[n_str_aux] = malloc(s); strncpy(str_aux[n_str_aux], p, s); @@ -449,6 +499,7 @@ case DC_SIGCHAR_STRING: return Py_BuildValue("s", dcCallPointer (gpCall, pfunc)); // !new ref! case DC_SIGCHAR_POINTER: return Py_BuildValue("n", dcCallPointer (gpCall, pfunc)); // !new ref! default: return PyErr_Format(PyExc_RuntimeError, "invalid return type signature"); + // @@@ this could be handled via array lookups of a 256b array instead of switch/case, then share it with callback code if it makes sense } #if !defined(PYUNICODE_CACHES_UTF8) @@ -469,6 +520,221 @@ } +#include "dyncall_callback.h" +#include "dyncall_args.h" + + +/* PyCObject destructor callback for callback obj */ + +#if defined(USE_CAPSULE_API) +static void free_callback(PyObject* capsule) +{ + void* cb = PyCapsule_GetPointer(capsule, NULL); +#else +static void free_callback(void* cb) +{ +#endif + if (cb != 0) + dcbFreeCallback(cb); +} + + +struct callback_userdata { + PyObject* f; + char sig[]; +}; + +/* generic callback handler dispatching to python */ +static char handle_py_callbacks(DCCallback* pcb, DCArgs* args, DCValue* result, void* userdata) +{ + + struct callback_userdata* x = (struct callback_userdata*)userdata; + const char* sig_ptr = x->sig; + + Py_ssize_t n_args = ((PyCodeObject*)PyFunction_GetCode(x->f))->co_argcount; + Py_ssize_t pos = 0; + PyObject* py_args = PyTuple_New(n_args); // !new ref! + PyObject* po; + char ch; + + if(py_args) + { + // @@@ we could do the below actually by using dyncall itself, piecing together python's sig string and then dcCallPointer(vm, Py_BuildValue, ...) + for (ch = *sig_ptr; ch != '\0' && ch != DC_SIGCHAR_ENDARG && pos < n_args; ch = *++sig_ptr) + { + switch(ch) + { + case DC_SIGCHAR_CC_PREFIX: assert(*(sig_ptr+1) == DC_SIGCHAR_CC_DEFAULT); /* not handling callbacks to anything but default callconf */ break; + case DC_SIGCHAR_BOOL: PyTuple_SET_ITEM(py_args, pos++, PyBool_FromLong(dcbArgBool (args))); break; // !new ref! (but "stolen" by SET_ITEM) + case DC_SIGCHAR_CHAR: PyTuple_SET_ITEM(py_args, pos++, Py_BuildValue("b", dcbArgChar (args))); break; // !new ref! (but "stolen" by SET_ITEM) + case DC_SIGCHAR_UCHAR: PyTuple_SET_ITEM(py_args, pos++, Py_BuildValue("B", dcbArgUChar (args))); break; // !new ref! (but "stolen" by SET_ITEM) + case DC_SIGCHAR_SHORT: PyTuple_SET_ITEM(py_args, pos++, Py_BuildValue("h", dcbArgShort (args))); break; // !new ref! (but "stolen" by SET_ITEM) + case DC_SIGCHAR_USHORT: PyTuple_SET_ITEM(py_args, pos++, Py_BuildValue("H", dcbArgUShort (args))); break; // !new ref! (but "stolen" by SET_ITEM) + case DC_SIGCHAR_INT: PyTuple_SET_ITEM(py_args, pos++, Py_BuildValue("i", dcbArgInt (args))); break; // !new ref! (but "stolen" by SET_ITEM) + case DC_SIGCHAR_UINT: PyTuple_SET_ITEM(py_args, pos++, Py_BuildValue("I", dcbArgUInt (args))); break; // !new ref! (but "stolen" by SET_ITEM) + case DC_SIGCHAR_LONG: PyTuple_SET_ITEM(py_args, pos++, Py_BuildValue("l", dcbArgLong (args))); break; // !new ref! (but "stolen" by SET_ITEM) + case DC_SIGCHAR_ULONG: PyTuple_SET_ITEM(py_args, pos++, Py_BuildValue("k", dcbArgULong (args))); break; // !new ref! (but "stolen" by SET_ITEM) + case DC_SIGCHAR_LONGLONG: PyTuple_SET_ITEM(py_args, pos++, Py_BuildValue("L", dcbArgLongLong (args))); break; // !new ref! (but "stolen" by SET_ITEM) + case DC_SIGCHAR_ULONGLONG: PyTuple_SET_ITEM(py_args, pos++, Py_BuildValue("K", dcbArgULongLong(args))); break; // !new ref! (but "stolen" by SET_ITEM) + case DC_SIGCHAR_FLOAT: PyTuple_SET_ITEM(py_args, pos++, Py_BuildValue("f", dcbArgFloat (args))); break; // !new ref! (but "stolen" by SET_ITEM) + case DC_SIGCHAR_DOUBLE: PyTuple_SET_ITEM(py_args, pos++, Py_BuildValue("d", dcbArgDouble (args))); break; // !new ref! (but "stolen" by SET_ITEM) + case DC_SIGCHAR_STRING: PyTuple_SET_ITEM(py_args, pos++, Py_BuildValue("s", dcbArgPointer (args))); break; // !new ref! (but "stolen" by SET_ITEM) + case DC_SIGCHAR_POINTER: PyTuple_SET_ITEM(py_args, pos++, Py_BuildValue("n", dcbArgPointer (args))); break; // !new ref! (but "stolen" by SET_ITEM) + default: /* will lead to "signature not matching" error */ pos = n_args; break; + // @@@ this could be handled via array lookups of a 256b array instead of switch/case, then share it with call code (for returns) if it makes sense + } + } + + + // we must be at end of sigstring, here + if(ch == ')') + { + po = PyEval_CallObject(x->f, py_args); + if(po) + { + // return value type + ch = *++sig_ptr; + + // @@@ copypasta from above, as a bit different, NO error handling right now, NO handling of 'Z', ... + switch(ch) + { + case DC_SIGCHAR_BOOL: + if ( !PyBool_Check(po) ) + PyErr_Format( PyExc_RuntimeError, "arg %d - expecting a bool", -1 ); + else + result->B = ((Py_True == po) ? DC_TRUE : DC_FALSE); + break; + + case DC_SIGCHAR_CHAR: + case DC_SIGCHAR_UCHAR: + py2dcchar(&result->c, po, ch == DC_SIGCHAR_UCHAR, -1); + break; + + case DC_SIGCHAR_SHORT: + case DC_SIGCHAR_USHORT: + py2dcshort(&result->s, po, ch == DC_SIGCHAR_USHORT, -1); + break; + + case DC_SIGCHAR_INT: + case DC_SIGCHAR_UINT: + if ( !DcPyInt_Check(po) ) + PyErr_Format( PyExc_RuntimeError, "arg %d - expecting an int", -1 ); + else + result->i = (DCint) DcPyInt_AS_LONG(po); + break; + + case DC_SIGCHAR_LONG: + case DC_SIGCHAR_ULONG: + if ( !DcPyInt_Check(po) ) + PyErr_Format( PyExc_RuntimeError, "arg %d - expecting an int", -1 ); + else + result->j = (DClong) PyLong_AsLong(po); + break; + + case DC_SIGCHAR_LONGLONG: + case DC_SIGCHAR_ULONGLONG: + py2dclonglong(&result->l, po, -1); + break; + + case DC_SIGCHAR_FLOAT: + if (!PyFloat_Check(po)) + PyErr_Format( PyExc_RuntimeError, "arg %d - expecting a float", -1 ); + else + result->f = (float)PyFloat_AsDouble(po); + break; + + case DC_SIGCHAR_DOUBLE: + if (!PyFloat_Check(po)) + PyErr_Format( PyExc_RuntimeError, "arg %d - expecting a float", -1 ); + else + result->d = PyFloat_AsDouble(po); + break; + + case DC_SIGCHAR_POINTER: // this will only accept integers, mutable array types (meaning only bytearray) or tuples describing a callback + py2dcpointer(&result->p, po, -1); + break; + } + + + Py_DECREF(po); + } + else + PyErr_SetString(PyExc_RuntimeError, "callback error: unknown error calling back python callback function"); + } + else + PyErr_Format(PyExc_RuntimeError, "callback error: python callback doesn't match signature argument count or signature wrong (invalid sig char or return type not specified)"); + + Py_DECREF(py_args); + } + else + PyErr_SetString(PyExc_RuntimeError, "callback error: unknown error creating python arg tuple"); + + // as callbacks might be called repeatedly we don't want the error indicator to pollute other calls, so print + if(PyErr_Occurred) { + PyErr_Print(); + return 'v'; // used as return char for errors @@@ unsure if smart, but it would at least indicate that no return value was set + } + + return ch; +} + + +/* new callback object function */ + +static PyObject* +pydc_new_callback(PyObject* self, PyObject* args) +{ + PyObject* f; + const char* sig; + struct callback_userdata* ud; + DCCallback* cb; + + if (!PyArg_ParseTuple(args, "sO", &sig, &f) || !PyFunction_Check(f)) + return PyErr_Format(PyExc_RuntimeError, "argument mismatch"); + + // pass signature and f (as borrowed ptr) in userdata; not incrementing f's refcount, + // b/c we can probably expect user making sure callback exists when its needed/called + ud = malloc(sizeof(struct callback_userdata) + strlen(sig)+1); + cb = dcbNewCallback(sig, handle_py_callbacks, ud); + if(!cb) { + free(ud); + Py_RETURN_NONE; + } + + ud->f = f; + strcpy(ud->sig, sig); + return DcPyCObject_FromVoidPtr(cb, &free_callback); // !new ref! +} + +/* free callback object function */ + +static PyObject* +pydc_free_callback(PyObject* self, PyObject* args) +{ + PyObject* pcobj; + void* cb; + + if (!PyArg_ParseTuple(args, "O", &pcobj)) + return PyErr_Format(PyExc_RuntimeError, "argument mismatch"); + + cb = DcPyCObject_AsVoidPtr(pcobj); + if (!cb) + return PyErr_Format(PyExc_RuntimeError, "cbhandle is NULL"); + + free(dcbGetUserData(cb)); // free helper struct callback_userdata + + dcbFreeCallback(cb); + DcPyCObject_SetVoidPtr(pcobj, NULL); + + //don't think I need to release it, as the pyobj is not equivalent to the held handle + //Py_XDECREF(pcobj); // release ref from pydc_load() + + Py_RETURN_NONE; +} + + + + // module deinit static void deinit_pydc(void* x) { @@ -499,11 +765,13 @@ PY_MOD_INIT_FUNC_NAME(void) { static PyMethodDef pydcMethods[] = { - {"load", pydc_load, METH_VARARGS, "load library" }, - {"find", pydc_find, METH_VARARGS, "find symbols" }, - {"free", pydc_free, METH_VARARGS, "free library" }, - {"get_path", pydc_get_path, METH_VARARGS, "get library path"}, - {"call", pydc_call, METH_VARARGS, "call function" }, + {"load", pydc_load, METH_VARARGS, "load library" }, + {"find", pydc_find, METH_VARARGS, "find symbols" }, + {"free", pydc_free, METH_VARARGS, "free library" }, + {"get_path", pydc_get_path, METH_VARARGS, "get library path" }, + {"call", pydc_call, METH_VARARGS, "call function" }, + {"new_callback", pydc_new_callback, METH_VARARGS, "new callback obj" }, // @@@ doc: only functions, not every callable, and only with positional args + {"free_callback", pydc_free_callback, METH_VARARGS, "free callback obj"}, {NULL,NULL,0,NULL} }; @@ -516,6 +784,9 @@ // NOTE: there is no way to pass a pointer to deinit_pydc - see PEP 3121 for details #endif + /* we convert pointers to python ints via Py_BuildValue('n', ...) which expects Py_ssize_t */ + assert(sizeof(Py_ssize_t) >= sizeof(void*)); + if(m) gpCall = dcNewCallVM(4096); //@@@ one shared callvm for the entire module, this is not reentrant diff -r 6387d39ecce2 -r 918dab7a6606 python/pydc/pydc.pyi --- a/python/pydc/pydc.pyi Fri Jan 22 15:18:56 2021 +0100 +++ b/python/pydc/pydc.pyi Tue Feb 02 20:42:02 2021 +0100 @@ -1,5 +1,5 @@ import sys -from typing import Optional, Any, TypeVar +from typing import Optional, Any, TypeVar, Callable # Handle type, depending on python version this is either internal type # PyCObject or PyCapsule, neither of one can be used as annotation @@ -10,4 +10,6 @@ def free(libhandle: H) -> None: ... def get_path(libhandle: Optional[H]) -> str: ... def call(funcptr: H, signature: str, *arguments: Any) -> Any: ... +def new_callback(c_signature: str, f: Callable) -> H: ... # note: only callbacks with positional args +def free_callback(cbhandle: H) -> None: ... diff -r 6387d39ecce2 -r 918dab7a6606 python/pydc/setup.py --- a/python/pydc/setup.py Fri Jan 22 15:18:56 2021 +0100 +++ b/python/pydc/setup.py Tue Feb 02 20:42:02 2021 +0100 @@ -2,12 +2,12 @@ pydcext = Extension('pydc', sources = ['pydc.c'] -, libraries = ['dyncall_s','dynload_s'] +, libraries = ['dyncall_s','dyncallback_s','dynload_s'] ) setup( name = 'pydc' -, version = '1.2.0' +, version = '1.2.5' , author = 'Daniel Adler, Tassilo Philipp' , author_email = 'dadler@dyncall.org, tphilip@dyncall.org' , maintainer = 'Daniel Adler, Tassilo Philipp' @@ -21,7 +21,9 @@ , description = 'dynamic call bindings for python' , long_description = ''' library allowing to call arbitrary C library functions dynamically, -based on a single call kernel (so no interface generation used/required) +based on a single call kernel (so no interface generation used/required); +also has callback support and helper functions to load shared objects and +lookup symbols by name (including from running process itself) ''' , package_data = {'': ['pydc.pyi']} , packages = ['']