Dear CFFI developers! Recently we have migrated one of our largest projects from our home-grown foreign-function interface implementation to CFFI. The project is almost 1M LOC with almost 1K of foreign-functions.
Previously, we have inlined all of our foreign-function stubs. We also use FTYPEs through the code for type-safety and optimization. Having CFFI functions inlined and FTYPEs generated would spare us from writing those declarations on our own. The attached patch contains a proposal that we would like to share. Please, let us know about any modification necessary that would make this patch useful for a larger community. FYI: We have tested the patch against about a hundredth open-source Lisp projects. Kindly, -- Andrzej Walczak (Google/ITA Software Engineer)
Added FTYPE declarations for DEFCFUN functions. Added INLINE for DEFCFUN in optimized mode. --- a/src/cffi-sbcl.lisp +++ b/src/cffi-sbcl.lisp @@ -46,6 +46,7 @@ #:with-foreign-pointer #:%foreign-funcall #:%foreign-funcall-pointer + #:%inlinep #:%foreign-type-alignment #:%foreign-type-size #:%load-foreign-library @@ -315,6 +316,10 @@ `(with-alien ((,function (* (function ,rettype ,@types)) ,ptr)) (alien-funcall ,function ,@fargs))))) +(defun %inlinep (&optional (env (and (boundp 'sb-c::*lexenv*) sb-c::*lexenv*))) + "True if DEFCFUN can be inlined in lexical environment ENV." + (sb-c:policy (or env sb-c::*policy*) (< debug 2 speed))) + ;;;# Callbacks ;;; The *CALLBACKS* hash table contains a direct mapping of CFFI --- a/src/early-types.lisp +++ b/src/early-types.lisp @@ -161,6 +161,14 @@ (:documentation "Unparse FOREIGN-TYPE to a type specification (symbol or list).")) +(defgeneric lisp-parameter-type (foreign-type) + (:documentation "Lisp type corresponding to FOREIGN-TYPE") + (:method (type) t)) + +(defgeneric lisp-value-type (foreign-type) + (:documentation "Lisp type for returning value corresponding to FOREIGN-TYPE") + (:method (type) `(values ,(lisp-parameter-type type) &optional))) + ;;;# Foreign Types (defclass foreign-type () @@ -194,6 +202,9 @@ (defmethod unparse-type ((type named-foreign-type)) (name type)) +(defmethod lisp-parameter-type ((type named-foreign-type)) + (lisp-parameter-type (unparse-type type))) + ;;;# Built-In Foreign Types (defclass foreign-built-in-type (foreign-type) @@ -224,6 +235,12 @@ "Returns the symbolic representation of a built-in type." (type-keyword type)) +(defmethod lisp-parameter-type ((type foreign-built-in-type)) + (lisp-parameter-type (type-keyword type))) + +(defmethod lisp-value-type ((type foreign-built-in-type)) + (lisp-value-type (type-keyword type))) + (defmethod print-object ((type foreign-built-in-type) stream) "Print a FOREIGN-TYPE instance to STREAM unreadably." (print-unreadable-object (type stream :type t :identity nil) @@ -364,6 +381,9 @@ "Return the size in bytes of a foreign typedef." (foreign-type-size (actual-type type))) +(defmethod lisp-parameter-type ((type foreign-type-alias)) + (lisp-parameter-type (actual-type type))) + (defclass foreign-typedef (foreign-type-alias named-foreign-type) ()) @@ -396,6 +416,12 @@ foreign-type-alias) ((unparsed-type :accessor unparsed-type))) +;;; +;;; The CFFI type can be translated to any type. +;;; Do not assume any dependency on the actual type. +(defmethod lisp-parameter-type ((type translatable-foreign-type)) 't) +(defmethod lisp-value-type ((type translatable-foreign-type)) '*) + ;;; If actual-type isn't parsed already, let's parse it. This way we ;;; don't have to export PARSE-TYPE and users don't have to worry ;;; about this in DEFINE-FOREIGN-TYPE or DEFINE-PARSE-METHOD. --- a/src/functions.lisp +++ b/src/functions.lisp @@ -214,21 +214,46 @@ (list rettype)) ,@options))))) +(eval-when (:compile-toplevel :load-toplevel :execute) + (unless (fboundp '%inlinep) + (defun %inlinep (&optional env) + "True if DEFCFUN can be inlined in lexical environment ENV." + (declare (ignore env)) + nil))) + (defun %defcfun (lisp-name foreign-name return-type args options docstring) (let* ((arg-names (mapcar #'first args)) (arg-types (mapcar #'second args)) - (syms (make-gensym-list (length args))) - (call-by-value (fn-call-by-value-p arg-types return-type))) + (syms (make-gensym-list (length (the list args)))) + (call-by-value (fn-call-by-value-p arg-types return-type)) + (inline + (destructuring-bind + (&key (inline (%inlinep) inlinep) &allow-other-keys) options + (when inlinep + (setf options (copy-list options)) + (remf options :inline)) + (and inline `((inline ,lisp-name))))) + (parsed-arg-types (mapcar #'parse-type arg-types)) + (parsed-return-type (parse-type return-type)) + (lisp-arg-types (mapcar #'lisp-parameter-type parsed-arg-types)) + (lisp-value-type (lisp-value-type parsed-return-type)) + (ftype `(function ,lisp-arg-types ,lisp-value-type)) + (declarations + (remove t (mapcar (lambda (arg type) `(type ,type ,arg)) + arg-names lisp-arg-types) + :key #'second))) (multiple-value-bind (prelude caller) (if call-by-value (values nil nil) (defcfun-helper-forms - foreign-name lisp-name (canonicalize-foreign-type return-type) - syms (mapcar #'canonicalize-foreign-type arg-types) options)) + foreign-name lisp-name (canonicalize parsed-return-type) + syms (mapcar #'canonicalize parsed-arg-types) options)) `(progn - ,prelude + ,@(when prelude `(,prelude)) + (declaim (ftype ,ftype ,lisp-name) ,@inline) (defun ,lisp-name ,arg-names ,@(ensure-list docstring) + ,@(when declarations `((declare ,@declarations))) ,(if call-by-value `(foreign-funcall ,(cons foreign-name options) --- a/src/package.lisp +++ b/src/package.lisp @@ -171,6 +171,8 @@ #:expand-to-foreign #:expand-from-foreign #:expand-into-foreign-memory + #:lisp-parameter-type + #:lisp-value-type ;; Foreign globals. #:defcvar --- a/src/strings.lisp +++ b/src/strings.lisp @@ -262,6 +262,11 @@ (print-unreadable-object (type stream :type t) (format stream "~S" (fst-encoding type)))) +(defmethod lisp-parameter-type ((type foreign-string-type)) + '(or foreign-pointer string)) +(defmethod lisp-value-type ((type foreign-string-type)) + '(values (or null string) &optional)) + (defmethod translate-to-foreign ((s string) (type foreign-string-type)) (values (foreign-string-alloc s :encoding (fst-encoding type)) (fst-free-to-foreign-p type))) @@ -303,3 +308,8 @@ (defmethod translate-from-foreign (value (type foreign-string+ptr-type)) (list (call-next-method) value)) + +(deftype foreign-string+ptr () '(cons (or null string) (cons foreign-pointer))) + +(defmethod lisp-value-type ((type foreign-string+ptr-type)) + '(values foreign-string+ptr &optional)) --- a/src/types.lisp +++ b/src/types.lisp @@ -1047,3 +1047,28 @@ (:uintptr . :pointer)) (:unsigned-char :unsigned-short :unsigned-int :unsigned-long :unsigned-long-long)))) + +;; Declare the Lisp type of built-in foreign-types. +(eval-when (:compile-toplevel :load-toplevel :execute) + (defmethod lisp-parameter-type ((type (eql :float))) 'single-float) + (defmethod lisp-parameter-type ((type (eql :double))) 'float) + (defmethod lisp-value-type ((type (eql :double))) + '(values double-float &optional)) + (defmethod lisp-parameter-type ((type (eql :string))) 'string) + (defmethod lisp-parameter-type ((type (eql :pointer))) 'foreign-pointer) + (defmethod lisp-parameter-type ((type foreign-boolean-type)) t) + (defmethod lisp-value-type ((type foreign-boolean-type)) + '(values boolean &optional)) + (defmethod lisp-value-type ((type (eql :void))) + '(values &optional)) + (macrolet + ((define (bytes types) + `(progn + ,@(loop :for key :in types :collect + `(defmethod lisp-parameter-type ((type (eql ,key))) + '(,bytes ,(* (foreign-type-size key) 8))))))) + (define signed-byte + (:char :short :int :long :long-long)) + (define unsigned-byte + (:unsigned-char :unsigned-short :unsigned-int + :unsigned-long :unsigned-long-long))))