;; ;; Python-Scheme FFI. Based on pyffi.lisp by Dmitri Hrapof. ;; Adapted to Chicken Scheme by Ivan Raikov. ;; Chicken Scheme code copyright 2007-2025 Ivan Raikov. ;; ;; ;; This program is free software: you can redistribute it and/or ;; modify it under the terms of the GNU General Public License as ;; published by the Free Software Foundation, either version 3 of the ;; License, or (at your option) any later version. ;; ;; This program is distributed in the hope that it will be useful, but ;; WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ;; General Public License for more details. ;; ;; A full copy of the GPL license can be found at ;; . ;; (module pyffi (py-start py-stop py-import py-eval py-apply py-object-type py-object-to py-object-from raise-python-exception python-exception-string *py-functions* (define-pyfun PyCallable_Check Py_DecRef) (define-pyslot PyObject_GetAttrString PyObject_SetAttrString ) (define-pymethod PyObject_GetAttrString PyObject_CallObject PyObject_Call parse-argument-list )) (import scheme (chicken base) (chicken foreign) (chicken keyword) (chicken syntax) (chicken blob) (chicken locative) (chicken string) (chicken condition) (only (chicken memory) pointer?) (only (chicken port) port-name) (only srfi-1 first second every break) srfi-4 srfi-69 bind utf8 utf8-lolevel utf8-srfi-13 utf8-srfi-14) (import-for-syntax (chicken base) (chicken keyword) (chicken string) (only srfi-1 first second every break) srfi-69) (define (pyffi:error x . rest) (let ((port (open-output-string))) (if (port? x) (begin (display "[" port) (display (port-name x) port) (display "] " port))) (let loop ((objs (if (port? x) rest (cons x rest)))) (if (null? objs) (begin (newline port) (error 'pyffi (get-output-string port))) (begin (display (car objs) port) (display " " port) (loop (cdr objs))))))) (define (alist? x) (and (list? x) (every pair? x))) (define-record pytype name to from) ;; Fixed macros using implicit renaming (ir-macro-transformer) ;; Only using inject when we need to break hygiene for specific bindings (define-syntax define-pytype (ir-macro-transformer (lambda (x inject compare) (let ((name (cadr x)) (to (caddr x)) (from (cadddr x))) `(define ,name (make-pytype ',name ,to ,from)))))) (define-syntax translate-to-foreign (ir-macro-transformer (lambda (x inject compare) (let ((x (cadr x)) (typ (caddr x))) `((pytype-to ,typ) ,x))))) (define-syntax translate-from-foreign (ir-macro-transformer (lambda (x inject compare) (let ((x (cadr x)) (typ (caddr x))) `((pytype-from ,typ) ,x))))) (define-syntax define-pyfun (ir-macro-transformer (lambda (x inject compare) (let* ((expr (cadr x)) (args (cddr x)) (func (gensym 'func)) ; Generate a unique name for the Scheme variable (name (if (list? expr) (second expr) (string->symbol expr))) (form (if (list? expr) (first expr) expr))) `(define ,(cons name args) (let ((,func (hash-table-ref/default ,(inject '*py-functions*) ',name #f))) (if (not ,func) (begin (set! ,func (py-eval ,form)) (if (not ,func) (raise-python-exception)) (if (not (PyCallable_Check ,func)) (begin (Py_DecRef ,func) (raise-python-exception))) (hash-table-set! ,(inject '*py-functions*) ',name ,func))) (py-apply ,func . ,args))))))) (define-syntax define-pyslot (ir-macro-transformer (lambda (x inject compare) (let ((name (cadr x)) (rest (cddr x))) (let-optionals rest ((scheme-name #f)) (let ((proc-name (or scheme-name (string->symbol name))) (obj (gensym 'obj)) (rest (gensym 'rest))) `(define (,proc-name ,obj . ,rest) (if (null? ,rest) (PyObject_GetAttrString ,obj ,(->string name)) (PyObject_SetAttrString ,obj ,(->string name) (car ,rest)))))))))) (define-syntax define-pymethod (ir-macro-transformer (lambda (x inject compare) (let ((name (cadr x)) (rest (cddr x))) (let ((scheme-name (member 'scheme-name: rest)) (kw (member 'kw: rest))) (let ((proc-name (or (and scheme-name (cadr scheme-name)) (string->symbol name))) (obj (gensym 'obj)) (rest (gensym 'rest))) (if (not kw) `(define (,proc-name ,obj #!rest ,rest) (PyObject_CallObject (PyObject_GetAttrString ,obj ,(->string name)) (list->vector ,rest))) (let ((keyword-symbols (cadr kw))) `(define ,proc-name ;; Create the keyword list once when the function is first defined (let ((cached-accepted-kwargs (map (lambda (sym) (string->keyword (symbol->string sym))) ',keyword-symbols))) ;; Return the actual function that uses the cached list (lambda (,obj #!rest ,rest) (receive (args kwargs) (parse-argument-list ,rest cached-accepted-kwargs) (PyObject_Call (PyObject_GetAttrString ,obj ,(->string name)) (list->vector args) (if (null? kwargs) '() kwargs)))))))))))))) ;; Scheme -> Python (define (py-object-to value) (cond ((exact-integer? value) (translate-to-foreign value py-int)) ((cplxnum? value) (translate-to-foreign value py-complex)) ((real? value) (translate-to-foreign value py-float)) ((alist? value) (translate-to-foreign value py-dict)) ((list? value) (if (eq? 'ascii (car value)) (translate-to-foreign (cadr value) py-ascii) (translate-to-foreign value py-list))) ((string? value) (translate-to-foreign value py-unicode)) ((vector? value) (translate-to-foreign value py-tuple)) ((blob? value) (translate-to-foreign value py-buffer)) ((boolean? value) (translate-to-foreign value py-bool)) ((pointer? value) value) (else (pyffi:error 'py-object-to "invalid value " value)))) ;; Python -> Scheme (define (py-object-from value) (if (not value) (raise-python-exception)) (let ((typ-name (py-object-type value))) (let ((typ-key (alist-ref typ-name *py-types* string=?))) (if typ-key (translate-from-foreign value typ-key) (begin (Py_IncRef value) value))))) (define (py-object-type value) (pyffi_PyObject_Type_toCString value)) ;; Embed, but do not parse #> #include #if ((PY_MAJOR_VERSION == 2) && (PY_MINOR_VERSION <= 3)) void Py_IncRef (PyObject *x) { Py_INCREF(x); } void Py_DecRef (PyObject *x) { Py_DECREF(x); } #endif PyObject *pyffi_PyImport_ImportModuleEx (char *, PyObject *, PyObject *, PyObject *); PyObject *pyffi_PyRun_String (const char *str, int s, PyObject *g, PyObject *l) { return PyRun_String (str, s, g, l); } PyObject *pyffi_PyImport_ImportModuleEx (char *name, PyObject *g, PyObject *l, PyObject *fl) { return PyImport_ImportModuleEx (name,g,l,fl); } C_word PyBool_asBool(PyObject *x) { if (x == (Py_True)) return C_SCHEME_TRUE; return C_SCHEME_FALSE; } #if PY_MAJOR_VERSION >= 3 #define PyInt_AsLong PyLong_AsLong #define PyInt_FromLong PyLong_FromLong #endif <# (define PyBool-AsBool (foreign-lambda scheme-object "PyBool_asBool" nonnull-c-pointer)) (bind-type pyobject (c-pointer "PyObject") py-object-to py-object-from) (define pyffi_PyUnicode_ref (foreign-lambda integer "pyffi_PyUnicode_ref" pyobject integer)) ;; Parse but do not embed (bind #<= 3 return PyUnicode_FromString(string); #else return PyBytes_FromString(string); #endif } char *pyffi_PyString_toCString(pyobject op) { #if PY_MAJOR_VERSION >= 3 const char *utf8 = PyUnicode_AsUTF8(op); if (!utf8) return NULL; return strdup(utf8); #else const char *bytes = PyBytes_AsString(op); if (!bytes) return NULL; return strdup(bytes); #endif } PyObject* pyffi_PyUnicode_fromCString(const char *string) { return PyUnicode_FromString(string); } char *pyffi_PyUnicode_toCString(pyobject op) { #if PY_MAJOR_VERSION >= 3 const char *utf8 = PyUnicode_AsUTF8(op); if (!utf8) return NULL; return strdup(utf8); #else const char *data = PyUnicode_AS_DATA(op); if (!data) return NULL; return strdup(data); #endif } long pyffi_PyUnicode_GetLength(pyobject op) { #if PY_MAJOR_VERSION >= 3 return PyUnicode_GET_LENGTH(op); #else return PyUnicode_GET_SIZE(op); #endif } char *pyffi_PyObject_Type_toCString (pyobject x) { PyObject *typ, *str; char *result; typ = PyObject_Type (x); if (!typ) return NULL; str = PyObject_Str (typ); Py_DecRef (typ); if (!str) return NULL; #if PY_MAJOR_VERSION >= 3 const char *utf8 = PyUnicode_AsUTF8(str); result = utf8 ? strdup(utf8) : NULL; #else const char *bytes = PyBytes_AsString(str); result = bytes ? strdup(bytes) : NULL; #endif Py_DecRef (str); return result; } char *pyffi_PyErr_Occurred_toCString (void) { PyObject *exc, *str; char *result; exc = PyErr_Occurred (); /* Borrowed reference */ if (!exc) return NULL; str = PyObject_Str (exc); if (!str) return NULL; #if PY_MAJOR_VERSION >= 3 const char *utf8 = PyUnicode_AsUTF8(str); result = utf8 ? strdup(utf8) : NULL; #else const char *bytes = PyBytes_AsString(str); result = bytes ? strdup(bytes) : NULL; #endif Py_DecRef (str); /* Only decref str, not exc */ return result; } /* Safe Unicode character access */ int pyffi_PyUnicode_ref (PyObject *x, int i) { if (!x || !PyUnicode_Check(x)) return 0; Py_ssize_t length = PyUnicode_GET_LENGTH(x); if (i < 0 || i >= length) return 0; Py_UCS4 char_code_point = PyUnicode_ReadChar(x, i); if (char_code_point == (Py_UCS4)-1 && PyErr_Occurred()) { return 0; } if (char_code_point > 0x10FFFF) return 0; /* Invalid Unicode */ return (int)char_code_point; } /* Memory cleanup utility */ void pyffi_free_string(char *str) { if (str) free(str); } int PyBuffer_Size(Py_buffer *buf) { return buf->len; } EOF ) (define (pyerror-exn x) (make-property-condition 'pyerror 'message x)) (define (raise-python-exception) (let* ((desc (pyffi_PyErr_Occurred_toCString))) (PyErr_Clear) (print-error-message desc) (signal (pyerror-exn desc)))) (define (python-exception-string) (let* ((desc (pyffi_PyErr_Occurred_toCString))) desc)) (define-pytype py-int PyInt_FromLong PyInt_AsLong) (define-pytype py-tuple (lambda (value) (let* ((len (vector-length value)) (tup (PyTuple_New len))) (if (not tup) (raise-python-exception)) (let loop ((i 0)) (if (< i len) (begin (if (not (zero? (PyTuple_SetItem tup i (vector-ref value i)))) (raise-python-exception)) (loop (+ 1 i))) tup)))) (lambda (value) (let* ((len (PyTuple_Size value)) (tup (make-vector len))) (let loop ((i 0)) (if (< i len) (begin (vector-set! tup i (PyTuple_GetItem value i)) (loop (+ 1 i))) tup))))) (define-pytype py-list (lambda (value) (let* ((len (length value)) (lst (PyList_New len))) (if (not lst) (raise-python-exception)) (let loop ((i 0)) (if (< i len) (begin (if (not (zero? (PyList_SetItem lst i (list-ref value i)))) (raise-python-exception)) (loop (+ i 1))) lst)))) (lambda (value) (let ((len (PyList_Size value))) (let loop ((i 0) (lst (list))) (if (< i len) (let ((item (PyList_GetItem value i))) (loop (+ 1 i) (cons item lst))) (begin (reverse lst))))))) (define-pytype py-bool (lambda (x) (PyBool_FromLong (if x 1 0))) PyBool-AsBool) (define-pytype py-float PyFloat_FromDouble PyFloat_AsDouble) (define-pytype py-complex (lambda (value) (let* ((r (real-part value)) (i (imag-part value))) (PyComplex_FromDoubles r i))) (lambda (value) (let* ((r (PyComplex_RealAsDouble value)) (i (PyComplex_ImagAsDouble value))) (make-rectangular r i)))) (define (utf8-string->py-unicode value) ;; Given a Scheme UTF8 string, converts it into Python Unicode string (let ((res (pyffi_PyUnicode_fromCString value))) res)) (define (py-unicode->utf8-string value) ;; Given a Python Unicode string, converts it into Scheme UTF8 string (let ((len (pyffi_PyUnicode_GetLength value))) (let loop ((i 0) (lst (list))) (if (< i len) (loop (+ 1 i) (cons (pyffi_PyUnicode_ref value i) lst)) (list->string (map integer->char (reverse lst))))))) (define-pytype py-ascii pyffi_PyString_fromCString pyffi_PyString_toCString) (define-pytype py-unicode utf8-string->py-unicode py-unicode->utf8-string) (define-pytype py-dict ;; Given a Scheme alist, converts it into a Python dictionary (lambda (value) (let ((dct (PyDict_New))) (if (not dct) (raise-python-exception)) (for-each (lambda (kv) (if (and (pair? kv) (pair? (cdr kv))) (let ((k (car kv)) (v (cadr kv))) (if (not (zero? (PyDict_SetItem dct k v))) (raise-python-exception))) (pyffi:error 'py-dict "invalid alist pair " kv))) value) dct )) ;; Given a Python dictionary, converts it into a Scheme alist (lambda (value) (let ((its (PyDict_Items value))) (let loop ((its its) (alst (list))) (if (null? its) alst (let ((item (car its))) (let ((k (vector-ref item 0)) (v (vector-ref item 1))) (loop (cdr its) (cons (list k v) alst))))))))) (define-pytype py-instance identity ;; Given a Python class instance, converts it into a Scheme alist (lambda (value) (PyObject_GetAttrString value "__dict__"))) (define-pytype py-buffer ;; Given a Scheme blob, converts it into a Python buffer (lambda (value) (let ((buf (make-blob (blob-size value)))) (if (not buf) (raise-python-exception)) (PyBuffer_FromContiguous (make-locative buf) (make-locative value) (blob-size value) #\C) buf )) ;; Given a Python buffer, converts it into a Scheme blob (lambda (value) (let ((b (make-blob (PyBuffer_Size value)))) (PyBuffer_ToContiguous (make-locative b) value (PyBuffer_Size value) #\C) b)) ) (define *py-types* `( ("" . ,py-bool) ("" . ,py-bool) ("" . ,py-int) ("" . ,py-float) ("" . ,py-complex) ("" . ,py-list) ("" . ,py-ascii) ("" . ,py-unicode) ("" . ,py-dict) ("" . ,py-instance) ("" . ,py-tuple) ("" . ,py-buffer) ) ) (define-constant +py-file-input+ 257) (define-constant +py-single-input+ 256) (define-constant +py-eval-input+ 258) (define *py-main-module* (make-parameter #f)) (define *py-main-module-dict* (make-parameter #f)) (define *py-functions* (make-hash-table eq? symbol-hash)) (define (py-start) (Py_Initialize) (*py-main-module* (PyImport_AddModule "__main__")) (*py-main-module-dict* (PyModule_GetDict_asPtr (*py-main-module*))) (Py_IncRef (*py-main-module-dict*))) (define (py-stop) (Py_DecRef (*py-main-module-dict*)) (*py-main-module* #f) (*py-main-module-dict* #f) (hash-table-clear! *py-functions*) (Py_Finalize)) (define (py-import name #!key (as #f)) (let* ((p (string-split name ".")) (id (if (null? p) name (car p)))) (let ((m (pyffi_PyImport_ImportModuleEx name (*py-main-module-dict*) #f '()))) (if m (if (< (PyModule_AddObject (*py-main-module*) id m) 0) (begin (Py_DecRef m) (raise-python-exception)) (begin (if as (PyModule_AddObject (*py-main-module*) as m) (Py_IncRef m)))) (raise-python-exception))) )) (define (py-eval expr) (pyffi_PyRun_String expr +py-eval-input+ (*py-main-module-dict*) #f)) (define (py-apply func . rest) (PyObject_CallObject func (list->vector rest))) (define (alistify-kwargs lst accepted-keywords) (let loop ((lst lst) (kwargs '())) (cond ((null? lst) kwargs) ((null? (cdr lst)) (pyffi:error 'alistify-kwargs "Bad keyword argument.")) ((not (keyword? (car lst))) (pyffi:error 'alistify-kwargs "Not a keyword: " (car lst))) ((not (member (car lst) accepted-keywords)) (pyffi:error 'alistify-kwargs "Unexpected keyword argument: " (car lst))) (#t (loop (cddr lst) (cons (list (keyword->string (car lst)) (cadr lst)) kwargs)))))) (define (parse-argument-list args accepted-keywords) (receive (args* kwargs*) (break keyword? args) (values args* (alistify-kwargs kwargs* accepted-keywords)))) )