Add `procedure->pointer' to the FFI.
authorLudovic Courtès <ludo@gnu.org>
Fri, 3 Sep 2010 13:12:54 +0000 (15:12 +0200)
committerLudovic Courtès <ludo@gnu.org>
Fri, 3 Sep 2010 13:26:37 +0000 (15:26 +0200)
* libguile/foreign.c (make_cif): New procedure, with code formerly in
  `scm_make_foreign_function'.
  (scm_make_foreign_function): Use it.
  (invoke_closure, scm_procedure_to_pointer)[FFI_CLOSURES]: New
  functions.

* libguile/foreign.h (scm_procedure_to_pointer): New declaration.

* module/system/foreign.scm: Export `procedure->pointer' when available.

* test-suite/standalone/test-ffi (f-callback-1, f-callback-2): New
  procedures and related tests.

* test-suite/standalone/test-ffi-lib.c (test_ffi_callback_1,
  test_ffi_callback_2): New functions.

* test-suite/tests/foreign.test ("procedure->pointer"): New test prefix.

* doc/ref/api-foreign.texi (Dynamic FFI): Document `procedure->pointer'.

doc/ref/api-foreign.texi
libguile/foreign.c
libguile/foreign.h
module/system/foreign.scm
test-suite/standalone/test-ffi
test-suite/standalone/test-ffi-lib.c
test-suite/tests/foreign.test

index bcb8798..88408ad 100644 (file)
@@ -803,8 +803,72 @@ by the foreign pointer is mutated in place.
 @end example
 
 As you can see, this interface to foreign functions is at a very low,
-somewhat dangerous level. A contribution to Guile in the form of a
-high-level FFI would be most welcome.
+somewhat dangerous level@footnote{A contribution to Guile in the form of
+a high-level FFI would be most welcome.}.
+
+@cindex callbacks
+The FFI can also work in the opposite direction: making Scheme
+procedures callable from C.  This makes it possible to use Scheme
+procedures as ``callbacks'' expected by C function.
+
+@deffn {Scheme Procedure} procedure->pointer return-type proc arg-types
+@deffnx {C Function} scm_procedure_to_pointer (return_type, proc, arg_types)
+Return a pointer to a C function of type @var{return-type}
+taking arguments of types @var{arg-types} (a list) and
+behaving as a proxy to procedure @var{proc}.  Thus
+@var{proc}'s arity, supported argument types, and return
+type should match @var{return-type} and @var{arg-types}.
+@end deffn
+
+As an example, here's how the C library's @code{qsort} array sorting
+function can be made accessible to Scheme (@pxref{Array Sort Function,
+@code{qsort},, libc, The GNU C Library Reference Manual}):
+
+@example
+(define qsort!
+  (let ((qsort (make-foreign-function void
+                                      (dynamic-func "qsort"
+                                                    (dynamic-link))
+                                      (list '* size_t size_t '*))))
+    (lambda (bv compare)
+      ;; Sort bytevector BV in-place according to comparison
+      ;; procedure COMPARE.
+      (let ((ptr (procedure->pointer int
+                                     (lambda (x y)
+                                       ;; X and Y are pointers so,
+                                       ;; for convenience, dereference
+                                       ;; them before calling COMPARE.
+                                       (compare (dereference-uint8* x)
+                                                (dereference-uint8* y)))
+                                     (list '* '*))))
+        (qsort (bytevector->pointer bv)
+               (bytevector-length bv) 1 ;; we're sorting bytes
+               ptr)))))
+
+(define (dereference-uint8* ptr)
+  ;; Helper function: dereference the byte pointed to by PTR.
+  (let ((b (pointer->bytevector ptr 1)))
+    (bytevector-u8-ref b 0)))
+
+(define bv
+  ;; An unsorted array of bytes.
+  (u8-list->bytevector '(7 1 127 3 5 4 77 2 9 0)))
+
+;; Sort BV.
+(qsort! bv (lambda (x y) (- x y)))
+
+;; Let's see what the sorted array looks like:
+(bytevector->u8-list bv)
+@result{} (0 1 2 3 4 5 7 9 77 127)
+@end example
+
+And voil@`a!
+
+Note that @code{procedure->pointer} is not supported (and not defined)
+on a few exotic architectures.  Thus, user code may need to check
+@code{(defined? 'procedure->pointer)}.  Nevertheless, it is available on
+many architectures, including (as of libffi 3.0.9) x86, ia64, SPARC,
+PowerPC, ARM, and MIPS, to name a few.
 
 @c Local Variables:
 @c TeX-master: "guile.texi"
index 33af172..c36972b 100644 (file)
@@ -563,19 +563,14 @@ fill_ffi_type (SCM type, ffi_type *ftype, ffi_type ***type_ptrs,
       ftype->elements[i] = NULL;
     }
 }
-    
-SCM_DEFINE (scm_make_foreign_function, "make-foreign-function", 3, 0, 0,
-            (SCM return_type, SCM func_ptr, SCM arg_types),
-            "Make a foreign function.\n\n"
-            "Given the foreign void pointer @var{func_ptr}, its argument and\n"
-            "return types @var{arg_types} and @var{return_type}, return a\n"
-            "procedure that will pass arguments to the foreign function\n"
-            "and return appropriate values.\n\n"
-            "@var{arg_types} should be a list of foreign types.\n"
-            "@code{return_type} should be a foreign type.")
-#define FUNC_NAME s_scm_make_foreign_function
+
+/* Return a "cif" (call interface) for the given RETURN_TYPE and
+   ARG_TYPES.  */
+static ffi_cif *
+make_cif (SCM return_type, SCM arg_types, const char *caller)
+#define FUNC_NAME caller
 {
-  SCM walk, scm_cif;
+  SCM walk;
   long i, nargs, n_structs, n_struct_elts;
   size_t cif_len;
   char *mem;
@@ -583,8 +578,6 @@ SCM_DEFINE (scm_make_foreign_function, "make-foreign-function", 3, 0, 0,
   ffi_type **type_ptrs;
   ffi_type *types;
 
-  SCM_VALIDATE_POINTER (2, func_ptr);
-
   nargs = scm_ilength (arg_types);
   SCM_ASSERT (nargs >= 0, arg_types, 3, FUNC_NAME);
   /* fixme: assert nargs < 1<<32 */
@@ -598,32 +591,31 @@ SCM_DEFINE (scm_make_foreign_function, "make-foreign-function", 3, 0, 0,
   for (walk = arg_types; scm_is_pair (walk); walk = scm_cdr (walk))
     if (!parse_ffi_type (scm_car (walk), 0, &n_structs, &n_struct_elts))
       scm_wrong_type_arg (FUNC_NAME, 3, scm_car (walk));
-  
+
   /* the memory: with space for the cif itself */
   cif_len = sizeof (ffi_cif);
 
   /* then ffi_type pointers: one for each arg, one for each struct
      element, and one for each struct (for null-termination) */
   cif_len = (ROUND_UP (cif_len, alignof(void*))
-             + (nargs + n_structs + n_struct_elts)*sizeof(void*));
-  
+            + (nargs + n_structs + n_struct_elts)*sizeof(void*));
+
   /* then the ffi_type structs themselves, one per arg and struct element, and
      one for the return val */
   cif_len = (ROUND_UP (cif_len, alignof(ffi_type))
-             + (nargs + n_struct_elts + 1)*sizeof(ffi_type));
+            + (nargs + n_struct_elts + 1)*sizeof(ffi_type));
 
   mem = scm_gc_malloc_pointerless (cif_len, "foreign");
-  scm_cif = scm_from_pointer (mem, NULL);
   cif = (ffi_cif *) mem;
 
   /* reuse cif_len to walk through the mem */
   cif_len = ROUND_UP (sizeof (ffi_cif), alignof(void*));
   type_ptrs = (ffi_type**)(mem + cif_len);
   cif_len = ROUND_UP (cif_len
-                      + (nargs + n_structs + n_struct_elts)*sizeof(void*),
-                      alignof(ffi_type));
+                     + (nargs + n_structs + n_struct_elts)*sizeof(void*),
+                     alignof(ffi_type));
   types = (ffi_type*)(mem + cif_len);
-  
+
   /* whew. now knit the pointers together. */
   cif->rtype = types++;
   fill_ffi_type (return_type, cif->rtype, &type_ptrs, &types);
@@ -640,12 +632,33 @@ SCM_DEFINE (scm_make_foreign_function, "make-foreign-function", 3, 0, 0,
   cif->nargs = nargs;
   cif->bytes = 0;
   cif->flags = 0;
-  
+
   if (FFI_OK != ffi_prep_cif (cif, FFI_DEFAULT_ABI, cif->nargs, cif->rtype,
-                              cif->arg_types))
-    scm_misc_error (FUNC_NAME, "ffi_prep_cif failed", SCM_EOL);
+                             cif->arg_types))
+    SCM_MISC_ERROR ("ffi_prep_cif failed", SCM_EOL);
+
+  return cif;
+}
+#undef FUNC_NAME
+
+SCM_DEFINE (scm_make_foreign_function, "make-foreign-function", 3, 0, 0,
+            (SCM return_type, SCM func_ptr, SCM arg_types),
+            "Make a foreign function.\n\n"
+            "Given the foreign void pointer @var{func_ptr}, its argument and\n"
+            "return types @var{arg_types} and @var{return_type}, return a\n"
+            "procedure that will pass arguments to the foreign function\n"
+            "and return appropriate values.\n\n"
+            "@var{arg_types} should be a list of foreign types.\n"
+            "@code{return_type} should be a foreign type.")
+#define FUNC_NAME s_scm_make_foreign_function
+{
+  ffi_cif *cif;
+
+  SCM_VALIDATE_POINTER (2, func_ptr);
 
-  return cif_to_procedure (scm_cif, func_ptr);
+  cif = make_cif (return_type, arg_types, FUNC_NAME);
+
+  return cif_to_procedure (scm_from_pointer (cif, NULL), func_ptr);
 }
 #undef FUNC_NAME
 
@@ -932,6 +945,81 @@ scm_i_foreign_call (SCM foreign, const SCM *argv)
 }
 
 \f
+/* Function pointers aka. "callbacks" or "closures".  */
+
+#ifdef FFI_CLOSURES
+
+/* Trampoline to invoke a libffi closure that wraps a Scheme
+   procedure.  */
+static void
+invoke_closure (ffi_cif *cif, void *ret, void **args, void *data)
+{
+  size_t i;
+  SCM proc, *argv, result;
+
+  proc = PTR2SCM (data);
+
+  argv = alloca (cif->nargs * sizeof (*argv));
+
+  /* Pack ARGS to SCM values, setting ARGV pointers.  */
+  for (i = 0; i < cif->nargs; i++)
+    argv[i] = pack (cif->arg_types[i], args[i]);
+
+  result = scm_call_n (proc, argv, cif->nargs);
+
+  unpack (cif->rtype, ret, result);
+}
+
+SCM_DEFINE (scm_procedure_to_pointer, "procedure->pointer", 3, 0, 0,
+           (SCM return_type, SCM proc, SCM arg_types),
+           "Return a pointer to a C function of type @var{return-type}\n"
+           "taking arguments of types @var{arg-types} (a list) and\n"
+           "behaving as a proxy to procedure @var{proc}.  Thus\n"
+           "@var{proc}'s arity, supported argument types, and return\n"
+           "type should match @var{return-type} and @var{arg-types}.\n")
+#define FUNC_NAME s_scm_procedure_to_pointer
+{
+  SCM pointer;
+  ffi_cif *cif;
+  ffi_status err;
+  void *closure, *executable;
+
+  cif = make_cif (return_type, arg_types, FUNC_NAME);
+
+  closure = ffi_closure_alloc (sizeof (ffi_closure), &executable);
+  err = ffi_prep_closure_loc ((ffi_closure *) closure, cif,
+                             invoke_closure, SCM2PTR (proc),
+                             executable);
+  if (err != FFI_OK)
+    {
+      ffi_closure_free (closure);
+      SCM_MISC_ERROR ("`ffi_prep_closure_loc' failed", SCM_EOL);
+    }
+
+  if (closure == executable)
+    pointer = scm_from_pointer (executable, ffi_closure_free);
+  else
+    {
+      /* CLOSURE needs to be freed eventually.  However, since
+        `GC_all_interior_pointers' is disabled, we can't just register
+        a finalizer for CLOSURE.  Instead, we create a pointer object
+        for CLOSURE, with a finalizer, and register it as a weak
+        reference of POINTER.  */
+      SCM friend;
+
+      pointer = scm_from_pointer (executable, NULL);
+      friend = scm_from_pointer (closure, ffi_closure_free);
+
+      register_weak_reference (pointer, friend);
+    }
+
+  return pointer;
+}
+#undef FUNC_NAME
+
+#endif /* FFI_CLOSURES */
+
+\f
 
 static void
 scm_init_foreign (void)
index f5fac51..1c57621 100644 (file)
@@ -95,6 +95,8 @@ SCM_INTERNAL SCM scm_pointer_to_string (SCM pointer);
 
 SCM_API SCM scm_make_foreign_function (SCM return_type, SCM func_ptr,
                                        SCM arg_types);
+SCM_API SCM scm_procedure_to_pointer (SCM return_type, SCM func_ptr,
+                                     SCM arg_types);
 SCM_INTERNAL SCM scm_i_foreign_call (SCM foreign, const SCM *argv);
 
 \f
index e9a4a7c..0ca7fbf 100644 (file)
@@ -43,6 +43,7 @@
             pointer->string
 
             make-foreign-function
+            ;; procedure->pointer (see below)
             make-c-struct parse-c-struct))
 
 (load-extension (string-append "libguile-" (effective-version))
@@ -57,6 +58,8 @@
   "Return true if POINTER is the null pointer."
   (= (pointer-address pointer) 0))
 
+(if (defined? 'procedure->pointer)
+    (export procedure->pointer))
 
 \f
 ;;;
index 5918a73..066d249 100755 (executable)
@@ -3,7 +3,9 @@ exec guile -q -s "$0" "$@"
 !#
 
 (use-modules (system foreign)
-             (rnrs bytevectors))
+             (rnrs bytevectors)
+             (srfi srfi-1)
+             (srfi srfi-26))
 
 (define lib
   (dynamic-link (string-append (getenv "builddir") "/libtest-ffi")))
@@ -175,6 +177,50 @@ exec guile -q -s "$0" "$@"
               '(0 1 2 3 4 5 6 7 0 0 0 0 0 0 0 0))
       (error "unexpected dest")))
 
+;;
+;; Function pointers
+;;
+
+(define f-callback-1
+  (make-foreign-function int (dynamic-func "test_ffi_callback_1" lib)
+                         (list '* int)))
+
+(if (defined? 'procedure->pointer)
+    (let* ((calls 0)
+           (ptr   (procedure->pointer int
+                                      (lambda (x)
+                                        (set! calls (+ 1 calls))
+                                        (* x 3))
+                                      (list int)))
+           (input (iota 123)))
+      (define (expected-result x)
+        (+ 7 (* x 3)))
+
+      (let ((result (map (cut f-callback-1 ptr <>) input)))
+        (and (or (= calls (length input))
+                 (error "incorrect number of callback calls" calls))
+             (or (equal? (map expected-result input) result)
+                 (error "incorrect result" result))))))
+
+(define f-callback-2
+  (make-foreign-function double (dynamic-func "test_ffi_callback_2" lib)
+                         (list '* float int double)))
+
+(if (defined? 'procedure->pointer)
+    (let* ((proc  (lambda (x y z)
+                    (* (+ x (exact->inexact y)) z)))
+           (ptr   (procedure->pointer double proc
+                                      (list float int double)))
+           (arg1 (map (cut * <> 1.25) (iota 123 500)))
+           (arg2 (iota 123))
+           (arg3 (map (cut / <> 2.0) (iota 123 0 -10))))
+      (define result
+        (map (cut f-callback-2 ptr <> <> <>)
+             arg1 arg2 arg3))
+
+      (or (equal? result (map proc arg1 arg2 arg3))
+          (error "incorrect result" result))))
+
 \f
 ;;;
 ;;; Global symbols.
index 8dec3d3..364e6a6 100644 (file)
@@ -213,3 +213,17 @@ void* test_ffi_memcpy (void *dest, void *src, scm_t_int32 n)
 {
   return memcpy (dest, src, n);
 }
+
+int test_ffi_callback_1 (int (*f) (int), int x);
+int test_ffi_callback_1 (int (*f) (int), int x)
+{
+  return f (x) + 7;
+}
+
+double test_ffi_callback_2 (double (*f) (float, int, double),
+                           float x, int y, double z);
+double test_ffi_callback_2 (double (*f) (float, int, double),
+                           float x, int y, double z)
+{
+  return f (x, y, z);
+}
index d93565e..fd42677 100644 (file)
         (string=? s (pointer->string (string->pointer s)))))))
 
 \f
+(with-test-prefix "procedure->pointer"
+
+  (define qsort
+    ;; Bindings for libc's `qsort' function.
+    (make-foreign-function void
+                           (dynamic-func "qsort" (dynamic-link))
+                           (list '* size_t size_t '*)))
+
+  (define (dereference-pointer-to-byte ptr)
+    (let ((b (pointer->bytevector ptr 1)))
+      (bytevector-u8-ref b 0)))
+
+  (define input
+    '(7 1 127 3 5 4 77 2 9 0))
+
+  (pass-if "qsort"
+    (if (defined? 'procedure->pointer)
+        (let* ((called? #f)
+               (cmp     (lambda (x y)
+                          (set! called? #t)
+                          (- (dereference-pointer-to-byte x)
+                             (dereference-pointer-to-byte y))))
+               (ptr     (procedure->pointer int cmp (list '* '*)))
+               (bv      (u8-list->bytevector input)))
+          (qsort (bytevector->pointer bv) (bytevector-length bv) 1
+                 (procedure->pointer int cmp (list '* '*)))
+          (and called?
+               (equal? (bytevector->u8-list bv)
+                       (sort input <))))
+        (throw 'unresolved)))
+
+  (pass-if-exception "qsort, wrong return type"
+    exception:wrong-type-arg
+
+    (if (defined? 'procedure->pointer)
+        (let* ((cmp     (lambda (x y) #f)) ; wrong return type
+               (ptr     (procedure->pointer int cmp (list '* '*)))
+               (bv      (u8-list->bytevector input)))
+          (qsort (bytevector->pointer bv) (bytevector-length bv) 1
+                 (procedure->pointer int cmp (list '* '*)))
+          #f)
+        (throw 'unresolved)))
+
+  (pass-if-exception "qsort, wrong arity"
+    exception:wrong-num-args
+
+    (if (defined? 'procedure->pointer)
+        (let* ((cmp     (lambda (x y z) #f)) ; wrong arity
+               (ptr     (procedure->pointer int cmp (list '* '*)))
+               (bv      (u8-list->bytevector input)))
+          (qsort (bytevector->pointer bv) (bytevector-length bv) 1
+                 (procedure->pointer int cmp (list '* '*)))
+          #f)
+        (throw 'unresolved))))
+
+\f
 (with-test-prefix "structs"
 
   (pass-if "parse-c-struct"