jolt could call C (foreign-fn -> foreign-procedure) but C could not call back into jolt, which GTK signals (and any callback-taking C API) require. Add the inverse: jolt.ffi/foreign-callable wraps a jolt fn as a C-callable function pointer, mirroring the foreign-fn pipeline. A new jolt.ffi/__ccallable special form carries the fn as a child expression (analyzed + walked by the passes; ir.clj gains an :ffi-callable arm in both child walks) plus literal arg/ret type keywords. The back end lowers it to a locked Chez foreign-callable and returns its entry-point address as a jolt pointer; host/chez/ffi.ss registers the code object so the collector keeps it, and free-callable unlocks it. :collect-safe emits the convention that reactivates the thread on entry, for callbacks fired while it is parked in a :blocking call (a GTK main loop). Test: ffi-binding-test.ss sorts an int array through libc qsort with a jolt comparator (C -> jolt -> C). Re-minted seed.
149 lines
7.9 KiB
Scheme
149 lines
7.9 KiB
Scheme
;; ffi.ss — the runtime side of jolt's foreign-function interface (jolt.ffi).
|
|
;;
|
|
;; A jolt LIBRARY binds native code itself: it loads a shared object and declares
|
|
;; typed foreign functions, then exposes a Clojure API. The TYPED CALL is lowered
|
|
;; at compile time to a Chez `foreign-procedure` by the backend (the
|
|
;; `jolt.ffi/foreign-fn` special form) — this file provides everything that does
|
|
;; NOT need compile-time types: loading libraries, allocating/reading/writing
|
|
;; foreign memory, and string/pointer marshaling. All exposed under `jolt.ffi`.
|
|
;;
|
|
;; A foreign pointer is a Chez machine address (an exact integer / uptr), the same
|
|
;; representation `void*` arguments and results use, so pointers flow between
|
|
;; foreign-fn calls and these helpers transparently.
|
|
|
|
;; --- loading shared objects --------------------------------------------------
|
|
;; (jolt.ffi/load-library name) loads a .so/.dylib by name (resolved by the OS
|
|
;; loader against the standard search paths). A library typically calls this once
|
|
;; at load with a platform-specific name. (load-library) with no name (or #f)
|
|
;; loads the running process's own symbols (libc, sockets).
|
|
(define (ffi-load-library . args)
|
|
(if (or (null? args) (jolt-nil? (car args)))
|
|
(begin (load-shared-object #f) jolt-nil)
|
|
(begin (load-shared-object (jolt-str-render-one (car args))) jolt-nil)))
|
|
|
|
(define (ffi-loaded? name)
|
|
(guard (e (#t #f)) (load-shared-object (jolt-str-render-one name)) #t))
|
|
|
|
;; --- foreign type keywords ---------------------------------------------------
|
|
;; The keyword type names jolt.ffi accepts (in foreign-fn signatures and the
|
|
;; memory accessors) map to Chez foreign types. Kept in one place so the backend
|
|
;; (compile-time, for foreign-procedure) and these accessors (runtime, for
|
|
;; foreign-ref/set!) agree.
|
|
(define (ffi-type->chez kw)
|
|
(let ((n (if (keyword-t? kw) (keyword-t-name kw) (jolt-str-render-one kw))))
|
|
(cond
|
|
((string=? n "int") 'int)
|
|
((string=? n "uint") 'unsigned-int)
|
|
((string=? n "long") 'long)
|
|
((string=? n "ulong") 'unsigned-long)
|
|
((string=? n "int64") 'integer-64)
|
|
((string=? n "uint64") 'unsigned-64)
|
|
((string=? n "size_t") 'size_t)
|
|
((string=? n "ssize_t") 'ssize_t)
|
|
((string=? n "iptr") 'iptr)
|
|
((string=? n "uptr") 'uptr)
|
|
((string=? n "double") 'double)
|
|
((string=? n "float") 'float)
|
|
((or (string=? n "pointer") (string=? n "void*")) 'void*)
|
|
((string=? n "string") 'string)
|
|
((string=? n "void") 'void)
|
|
((or (string=? n "uint8") (string=? n "u8") (string=? n "byte")) 'unsigned-8)
|
|
((string=? n "char") 'char)
|
|
(else (error #f (string-append "jolt.ffi: unknown foreign type :" n))))))
|
|
|
|
;; --- foreign memory ----------------------------------------------------------
|
|
;; alloc returns a pointer (integer address). The caller frees it. read/write take
|
|
;; a type keyword and an optional byte offset.
|
|
(define (ffi-alloc nbytes) (foreign-alloc (jnum->exact nbytes)))
|
|
(define (ffi-free ptr) (foreign-free (jnum->exact ptr)) jolt-nil)
|
|
(define (ffi-read ptr ty . off)
|
|
(foreign-ref (ffi-type->chez ty) (jnum->exact ptr) (if (pair? off) (jnum->exact (car off)) 0)))
|
|
(define (ffi-write ptr ty off val)
|
|
(foreign-set! (ffi-type->chez ty) (jnum->exact ptr) (jnum->exact off) val) jolt-nil)
|
|
;; sizeof a foreign type (for laying out structs / arrays).
|
|
(define (ffi-sizeof ty) (foreign-sizeof (ffi-type->chez ty)))
|
|
(define (ffi-null? ptr) (and (number? ptr) (= (jnum->exact ptr) 0)))
|
|
(define ffi-null 0)
|
|
|
|
;; --- buffer I/O (known length) ----------------------------------------------
|
|
;; read n bytes at ptr as a string (UTF-8, falling back to latin1 for invalid
|
|
;; sequences) — for a socket recv buffer and similar fixed-length reads.
|
|
(define (ffi-read-bytes ptr n)
|
|
(let* ((n (jnum->exact n)) (p (jnum->exact ptr)) (bv (make-bytevector n)))
|
|
(do ((i 0 (+ i 1))) ((= i n)) (bytevector-u8-set! bv i (foreign-ref 'unsigned-8 p i)))
|
|
(guard (e (#t (list->string (map integer->char (bytevector->u8-list bv))))) (utf8->string bv))))
|
|
;; write a string's UTF-8 bytes into ptr (no NUL terminator); return the count.
|
|
(define (ffi-write-bytes ptr s)
|
|
(let* ((bv (string->utf8 (jolt-str-render-one s))) (n (bytevector-length bv)) (p (jnum->exact ptr)))
|
|
(do ((i 0 (+ i 1))) ((= i n)) (foreign-set! 'unsigned-8 p i (bytevector-u8-ref bv i)))
|
|
n))
|
|
(def-var! "jolt.ffi" "read-bytes" ffi-read-bytes)
|
|
(def-var! "jolt.ffi" "write-bytes" ffi-write-bytes)
|
|
|
|
;; --- byte-array buffer I/O (binary-faithful) --------------------------------
|
|
;; Move raw bytes between a jolt byte-array (jolt-array kind 'byte) and foreign
|
|
;; memory, byte-exact (no UTF-8 / latin1 decode) — for socket recv/send and the
|
|
;; zlib / OpenSSL buffers an HTTP client passes through. read-array returns a
|
|
;; fresh byte-array of n bytes; write-array copies a byte-array's bytes into ptr
|
|
;; and returns the count.
|
|
(define (ffi-read-array ptr n)
|
|
(let* ((n (jnum->exact n)) (p (jnum->exact ptr)) (v (make-vector n 0)))
|
|
(do ((i 0 (+ i 1))) ((= i n)) (vector-set! v i (foreign-ref 'unsigned-8 p i)))
|
|
(make-jolt-array v 'byte)))
|
|
(define (ffi-write-array ptr arr)
|
|
(let* ((v (jolt-array-vec arr)) (n (vector-length v)) (p (jnum->exact ptr)))
|
|
(do ((i 0 (+ i 1))) ((= i n)) (foreign-set! 'unsigned-8 p i (bitwise-and (exact (vector-ref v i)) #xff)))
|
|
n))
|
|
(def-var! "jolt.ffi" "read-array" ffi-read-array)
|
|
(def-var! "jolt.ffi" "write-array" ffi-write-array)
|
|
|
|
;; --- string / bytevector marshaling ------------------------------------------
|
|
;; A C string result already comes back as a jolt string (the `string` foreign
|
|
;; type). For a `void*` that points at a NUL-terminated C string, read it here.
|
|
(define (ffi-ptr->string ptr)
|
|
(if (ffi-null? ptr) jolt-nil
|
|
(let ((p (jnum->exact ptr)))
|
|
(let loop ((i 0) (acc '()))
|
|
(let ((b (foreign-ref 'unsigned-8 p i)))
|
|
(if (= b 0) (utf8->string (u8-list->bytevector (reverse acc)))
|
|
(loop (+ i 1) (cons b acc))))))))
|
|
;; Copy a jolt string's UTF-8 bytes into a freshly alloc'd NUL-terminated buffer;
|
|
;; the caller frees it. Returns the pointer.
|
|
(define (ffi-string->ptr s)
|
|
(let* ((bv (string->utf8 (jolt-str-render-one s))) (n (bytevector-length bv))
|
|
(p (foreign-alloc (+ n 1))))
|
|
(do ((i 0 (+ i 1))) ((= i n)) (foreign-set! 'unsigned-8 p i (bytevector-u8-ref bv i)))
|
|
(foreign-set! 'unsigned-8 p n 0)
|
|
p))
|
|
|
|
;; --- callbacks: receive calls FROM C ----------------------------------------
|
|
;; jolt.ffi/foreign-callable lowers to (jolt-ffi-register-callable! (foreign-callable …)).
|
|
;; A foreign-callable code object must be LOCKED (so the collector neither moves
|
|
;; nor reclaims it) and RETAINED while C may still call through its entry point.
|
|
;; Register it keyed by that entry-point address (a jolt pointer integer) — which
|
|
;; is what the caller hands to C; free-callable unlocks and drops it. A callback
|
|
;; left registered lives for the process (the GTK-signal-handler common case).
|
|
(define ffi-callable-table (make-eqv-hashtable)) ; entry-point addr -> code object
|
|
(define (jolt-ffi-register-callable! co)
|
|
(lock-object co)
|
|
(let ((addr (foreign-callable-entry-point co)))
|
|
(hashtable-set! ffi-callable-table addr co)
|
|
addr))
|
|
(define (ffi-free-callable addr)
|
|
(let* ((a (jnum->exact addr)) (co (hashtable-ref ffi-callable-table a #f)))
|
|
(when co (unlock-object co) (hashtable-delete! ffi-callable-table a))
|
|
jolt-nil))
|
|
|
|
;; --- expose under jolt.ffi ---------------------------------------------------
|
|
(def-var! "jolt.ffi" "free-callable" ffi-free-callable)
|
|
(def-var! "jolt.ffi" "load-library" ffi-load-library)
|
|
(def-var! "jolt.ffi" "loaded?" (lambda (n) (if (ffi-loaded? n) #t #f)))
|
|
(def-var! "jolt.ffi" "alloc" ffi-alloc)
|
|
(def-var! "jolt.ffi" "free" ffi-free)
|
|
(def-var! "jolt.ffi" "read" ffi-read)
|
|
(def-var! "jolt.ffi" "write" ffi-write)
|
|
(def-var! "jolt.ffi" "sizeof" ffi-sizeof)
|
|
(def-var! "jolt.ffi" "null?" (lambda (p) (if (ffi-null? p) #t #f)))
|
|
(def-var! "jolt.ffi" "null" ffi-null)
|
|
(def-var! "jolt.ffi" "ptr->string" ffi-ptr->string)
|
|
(def-var! "jolt.ffi" "string->ptr" ffi-string->ptr)
|