Documentation: Callbacks from Tcl to CMUCL |
This page describes how to use the CMUCL FFI to allow callbacks from foreign code into Lisp. This implementation only works when using the generational garbage collector (you should have :gencgc in your *features* list; this is the case on all x86 platforms). It is also possible to make it work with the non-generational garbage collector; ask the cmucl-help mailing list. There is some glue code in Lisp to save into cl-tcl.lisp, some C glue code to save into tcl-callback.c, and some demo code at the end.
The following code should be saved into a file cl-tcl.lisp:
;;; Evaluate (alien:load-foreign "/usr/lib/libtcl8.0.so") and ;;; (alien:load-foreign "tcl-demo.o") before compiling or loading ;;; this file. You will also likely need a version of CMUCL where ;;; the /usr/bin/lisp binary has been produced with the flags ;;; -Xlinker --export-dynamic added to OS_LINK_FLAGS in Config.linux* ;;; Note that the Debian binaries usually don't include these flags. (cl:defpackage "CL-TCL" (:use "CL" "ALIEN" "C-CALL" "SYSTEM") (:export "+TCL-OK+" "+TCL-ERROR+" "+TCL-RETURN+" "+TCL-BREAK+" "+TCL-CONTINUE+" "TCL-INTERPRETER" "CREATE-TCL-INTERPRETER" "DESTROY-TCL-INTERPRETER" "REGISTER-TCL-COMMAND" "UNREGISTER-TCL-COMMAND" "WITH-TCL-INTERPRETER" "EVAL-TCL-EXPR")) (in-package :CL-TCL) ;;;; User and Implementation-level constants (defconstant +TCL-OK+ 0 "TCL return code: Command completed normally; the interpreter's result contains the command's result.") (defconstant +TCL-ERROR+ 1 "TCL return code: The command couldn't be completed successfully; the interpreter's result describes what went wrong.") (defconstant +TCL-RETURN+ 2 "TCL return code: The command requests that the current procedure return; the interpreter's result contains the procedure's return value.") (defconstant +TCL-BREAK+ 3 "TCL return code: The command requests that the innermost loop be exited; the interpreter's result is meaningless.") (defconstant +TCL-CONTINUE+ 4 "TCL return code: Go on to the next iteration of the current loop; the interpreter's result is meaningless.") ;;;; FFI bindings to the Tcl Library and the C-side of the callback stuff (declaim (inline tcl-createinterp)) (def-alien-routine "Tcl_CreateInterp" system-area-pointer) (declaim (inline tcl-eval)) (def-alien-routine "Tcl_Eval" int (interp system-area-pointer :in) (string c-string :in)) (declaim (inline tcl-getstringresult)) (def-alien-routine "Tcl_GetStringResult" c-string (interp system-area-pointer :in)) (declaim (inline tcl-deleteinterp)) (def-alien-routine "Tcl_DeleteInterp" void (interp system-area-pointer :in)) (declaim (inline set-lisp-callback-handler)) (def-alien-routine "Set_Lisp_Callback_Handler" void (handler unsigned-long :in)) (declaim (inline register-tcl-lisp-command)) (def-alien-routine "Register_Tcl_Lisp_Command" void (interp system-area-pointer :in) (name c-string :in)) (declaim (inline unregister-tcl-lisp-command)) (def-alien-routine "Unregister_Tcl_Lisp_Command" void (interp system-area-pointer :in) (name c-string :in)) ;;;; User-level stuff (defstruct (tcl-interpreter (:print-function print-tcl-interpreter)) (valid-p t) (sap (int-sap 0) :type system-area-pointer) (commands (make-hash-table :test #'equal) :read-only t)) (defun print-tcl-interpreter (inter stream depth) (declare (ignore depth)) (print-unreadable-object (inter stream :type t :identity t) (format stream "~:[(INVALID)~;(valid@~8,'0X)~]" (tcl-interpreter-valid-p inter) (sap-int (tcl-interpreter-sap inter))))) (defvar *tcl-interpreter-list* nil "List of valid TCL interpreters.") (defun create-tcl-interpreter () "Create a TCL interpreter object that can be passed to the other functions of CL-TCL." (let ((result (make-tcl-interpreter :sap (tcl-createinterp)))) (push result *tcl-interpreter-list*) result)) (defun destroy-tcl-interpreter (inter) "Destroy the TCL interpreter object `inter'." (assert (and (tcl-interpreter-valid-p inter) (member inter *tcl-interpreter-list*))) (setf *tcl-interpreter-list* (delete (tcl-interpreter-sap inter) *tcl-interpreter-list* :key #'tcl-interpreter-sap :test #'sap=)) (tcl-deleteinterp (tcl-interpreter-sap inter)) (setf (tcl-interpreter-valid-p inter) nil (tcl-interpreter-sap inter) (int-sap 0)) inter) (defun register-tcl-command (inter name handler) "Registers the lisp function `handler' as the handler for the command named by the string `name' in the TCL-Interpreter `inter'. Any previously existing handlers will be superseded. When the corresponding command is invoked in the TCL interpreter, the handler will be called with the TCL-Interpreter object, the command name and all the arguments to the TCL command. The handler must return two values: The return code (one of +TCL-OK+, +TCL-ERROR+, +TCL-RETURN+, +TCL-BREAK+ or +TCL-CONTINUE+) and the result or error indicator string." (assert (tcl-interpreter-valid-p inter)) (setf (gethash name (tcl-interpreter-commands inter)) handler) (register-tcl-lisp-command (tcl-interpreter-sap inter) name)) (defun unregister-tcl-command (inter name) "Unregisters the previously registered command named `name' in the TCL-Interpreter `inter'. Returns the old handler function." (assert (tcl-interpreter-valid-p inter)) (let ((handler (gethash name (tcl-interpreter-commands inter) nil))) (assert handler) (remhash name (tcl-interpreter-commands inter)) (unregister-tcl-lisp-command (tcl-interpreter-sap inter) name) handler)) (defmacro with-tcl-interpreter (inter (&rest commands) &body body) "Evaluate the body in an environment were `inter' is bound to a Tcl interpreter instance and for each (name tcl-proc) in `commands' a Tcl command for name is defined, which invokes tcl-proc." `(let ((,inter (create-tcl-interpreter))) (unwind-protect (progn ,@(loop for (name handler) in commands collect `(register-tcl-command ,inter ,name ,handler)) ,@body) (destroy-tcl-interpreter ,inter)))) (defun eval-tcl-expr (inter expr) "Evaluate the expression (string) `expr' in the Tcl-Interpreter `inter' and return as multiple values the return code and result string." (assert (tcl-interpreter-valid-p inter)) (let* ((result-code (tcl-eval (tcl-interpreter-sap inter) expr)) (result-string (tcl-getstringresult (tcl-interpreter-sap inter)))) (values result-code result-string))) ;;;; Lisp-side of the callback stuff. (defun %lisp-callback-handler (inter-sap args) (handler-case (let ((inter (find inter-sap *tcl-interpreter-list* :key #'tcl-interpreter-sap :test #'sap=))) (assert (and inter (tcl-interpreter-valid-p inter))) (let ((handler (gethash (car args) (tcl-interpreter-commands inter) nil))) (assert handler) (multiple-value-bind (code result) (apply handler inter args) (cons code result)))) (error (c) (cons +TCL-ERROR+ (format nil "Lisp error: ~A" c))))) (defun %update-lisp-callback-handler () (set-lisp-callback-handler (kernel:get-lisp-obj-address #'%lisp-callback-handler))) (defvar *lisp-callback-scavhook* (ext:make-scavenger-hook :value #'%lisp-callback-handler :function #'%update-lisp-callback-handler)) (%update-lisp-callback-handler)
The following C code should be saved into a file tcl-callback.c.
#include <stdlib.h> #include <string.h> #include <tcl.h> #include "lisp.h" #include "internals.h" #include "alloc.h" #include "arch.h" lispobj lisp_callback_handler; static lispobj alloc_str_list(char *list[]) { lispobj result, newcons; struct cons *ptr; if (*list == NULL) result = NIL; else { result = newcons = alloc_cons(alloc_string(*list++), NIL); while (*list != NULL) { ptr = (struct cons *)PTR(newcons); newcons = alloc_cons(alloc_string(*list++), NIL); ptr->cdr = newcons; } } return result; } int LispTclProc(ClientData data,Tcl_Interp *interp,int argc,char** argv) { lispobj lisp_result; struct vector *vec; char* result; int returnvalue; lisp_result=funcall2(lisp_callback_handler,alloc_sap(interp), alloc_str_list(argv)); returnvalue=fixnum_value(CONS(lisp_result)->car); vec=(struct vector*)PTR(CONS(lisp_result)->cdr); result=ckalloc((vec->length)+1); strcpy(result,(char*)vec->data); interp->result=result; interp->freeProc=TCL_DYNAMIC; return returnvalue; } void Set_Lisp_Callback_Handler(lispobj handler) { lisp_callback_handler=handler; } void Register_Tcl_Lisp_Command(Tcl_Interp* interp,char* name) { Tcl_CreateCommand(interp,name,LispTclProc,NULL,NULL); } void Unregister_Tcl_Lisp_Command(Tcl_Interp* interp,char* name) { Tcl_DeleteCommand(interp,name); }
Here is some code you can use to load the demo into CMUCL:
(alien:load-foreign "/usr/lib/libtcl8.0.so") (alien:load-foreign (make-pathname :name "tcl-callback" :type "o" :version nil :defaults *load-truename*)) (load (compile-file (make-pathname :name "cl-tcl" :type "cl" :version nil :defaults *load-truename*)))
And finally, here is some sample code demonstrating the use of a tcl function which calls back into Lisp:
(cl:defpackage "CL-TCL-DEMO" (:use "CL" "CL-TCL") (:export "TCL-DEMO")) (in-package :CL-TCL-DEMO) (defun tcl-incr (inter cmd first &optional second) (declare (ignore inter cmd)) (values 0 (format nil "~D" (+ (parse-integer first) (if second (parse-integer second) 1))))) (defun tcl-demo (n) (with-tcl-interpreter inter (("lincr" #'tcl-incr)) (multiple-value-bind (code result) (eval-tcl-expr inter (format nil "set result \"(\" for {set i 1} {$i <= ~D} {set i [lincr $i 2]} {append result $i \" \"} append result \")\" return $result" n)) (case code ((#.+TCL-OK+ #.+TCL-RETURN+) (read-from-string result)) (t (values code result))))))
This description is adapted from the article <87k8jzthta.fsf@orion.dent.isdn.cs.tu-berlin.de> posted to the USENET group comp.lang.lisp on 2000-02-21 by Pierre Mai.