changeset 54:918dab7a6606

- added callback support (comes with some bigger refactoring) - allow CPython's Py{CObject,Capsule} to be used as 'p'ointers
author Tassilo Philipp
date Tue, 02 Feb 2021 20:42:02 +0100
parents 6387d39ecce2
children 2e8a56976bf8
files python/pydc/README.txt python/pydc/examples/callback.py python/pydc/pydc.c python/pydc/pydc.pyi python/pydc/setup.py
diffstat 5 files changed, 448 insertions(+), 107 deletions(-) [+]
line wrap: on
line diff
--- 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
--- /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)
+
--- 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 <Python.h>
 #include "dynload.h"
 #include <limits.h>
+#include <assert.h>
 
 
 
@@ -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
 
--- 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: ...
 
--- 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         = ['']