Attempt to use Scheme procedure as callback from C pasted by jcroisant on Fri Jan 21 06:33:58 2022

;;; GOAL: Create a wrapper around the example C function below, so
;;; that users can pass a normal Scheme procedure as a callback. The
;;; module can be compiled, but users should be able to use it from
;;; csi, passing a non-compiled Scheme procedure as the callback.
;;;
;;; CHALLENGE: The callback procedure might trigger the GC, which
;;; could cause the callback procedure itself to move while the C
;;; function is still iterating.

(module foo (foo-iterate)
  (import scheme
          (chicken base)
          (chicken foreign))

  ;; Minimal example C library.
  (foreign-declare "
   typedef void (*fooCallback)(int i, void *data);

   void fooIterate(int loops, fooCallback callback, void *data) {
     for (int i=0; i < loops; i++) {
       callback(i, data);
     }
   }")

  ;; Treats the data pointer as a Scheme procedure and calls it.
  ;; Not reliable!
  (define-external
    (callbackAdapter (int i) (scheme-object data))
    void
    (assert (procedure? data)
            "Procedure invalid" data i)
    (data i))

  ;; Binding to fooIterate which uses callbackAdapter as the callback,
  ;; passing along the Scheme procedure as the data pointer.
  (define foo-iterate
    (foreign-safe-lambda*
     void
     ((int loops)
      (scheme-object proc))
     "fooIterate(loops, (fooCallback)callbackAdapter, (void*)proc);")))


;;; Everything below should be able to run in csi.

(import foo)

(define *results* (make-parameter '()))
(define (push-result! i)
  (*results* (cons i (*results*))))

;;; Usually works ok with a small number of iterations.
(print "Running with 50")
(foo-iterate 50 push-result!)
(print "Length of results: " (length (*results*)))

;;; Fails after a while, presumably because the GC runs and relocates
;;; push-result!, and fooIterate is still using the old location.
(print "Running with 1,000,000")
(foo-iterate 1000000 push-result!)
(print "Length of results: " (length (*results*)))


;;; $ csc foo.scm; ./foo
;;;
;;; Running with 50
;;; Length of results: 50
;;; Running with 1,000,000
;;;
;;; Error: (foo.scm:30) Procedure invalid
;;; #<invalid forwarded object>
;;; 11418
;;;
;;; 	Call history:
;;;
;;; 	foo.scm:32: data
;;; 	foo.scm:50: *results*
;;; 	foo.scm:50: *results*
;;; 	foo.scm:32: data
;;; 	foo.scm:50: *results*
;;; 	foo.scm:50: *results*
;;; 	foo.scm:32: data
;;; 	foo.scm:50: *results*
;;; 	foo.scm:50: *results*
;;; 	foo.scm:32: data
;;; 	foo.scm:50: *results*
;;; 	foo.scm:50: *results*
;;; 	foo.scm:32: data
;;; 	foo.scm:50: *results*
;;; 	foo.scm:50: *results*
;;; 	foo.scm:30: ##sys#error	  	<--

possible solution added by klm on Fri Jan 21 18:55:17 2022

;;; GOAL: Create a wrapper around the example C function below, so
;;; that users can pass a normal Scheme procedure as a callback. The
;;; module can be compiled, but users should be able to use it from
;;; csi, passing a non-compiled Scheme procedure as the callback.
;;;
;;; CHALLENGE: The callback procedure might trigger the GC, which
;;; could cause the callback procedure itself to move while the C
;;; function is still iterating.

(module foo (foo-iterate)
  (import scheme
          (chicken base)
          (chicken foreign))

  ;; Minimal example C library.
  (foreign-declare "
   typedef void* (*fooCallback)(int i, void *data);

   void fooIterate(int loops, fooCallback callback, void *data) {
     for (int i=0; i < loops; i++) {
        data = callback(i, data);
     }
   }")

  ;; Treats the data pointer as a Scheme procedure and calls it.
  ;; Not reliable!
  (define-external
    (callbackAdapter (int i) (scheme-object data))
    scheme-object
    (assert (procedure? data)
            "Procedure invalid" data i)
    (data i)
    data)

  ;; Binding to fooIterate which uses callbackAdapter as the callback,
  ;; passing along the Scheme procedure as the data pointer.
  (define foo-iterate
    (foreign-safe-lambda*
     void
     ((int loops)
      (scheme-object proc))
     "fooIterate(loops, (fooCallback)callbackAdapter, (void*)proc);")))


;;; Everything below should be able to run in csi.

(import foo)

(define *results* (make-parameter '()))
(define (push-result! i)
  (*results* (cons i (*results*))))

;;; Usually works ok with a small number of iterations.
(print "Running with 50")
(foo-iterate 50 push-result!)
(print "Length of results: " (length (*results*)))

;;; Fails after a while, presumably because the GC runs and relocates
;;; push-result!, and fooIterate is still using the old location.
(print "Running with 1,000,000")
(foo-iterate 1000000 push-result!)
(print "Length of results: " (length (*results*)))