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))))