;;;; -*- Mode: lisp; indent-tabs-mode: nil -*- ;;; ;;; cffi-abcl.lisp --- CFFI-SYS implementation for ABCL/JNA. ;;; ;;; Copyright (C) 2009, Luis Oliveira ;;; Copyright (C) 2012, Mark Evenson ;;; ;;; Permission is hereby granted, free of charge, to any person ;;; obtaining a copy of this software and associated documentation ;;; files (the "Software"), to deal in the Software without ;;; restriction, including without limitation the rights to use, copy, ;;; modify, merge, publish, distribute, sublicense, and/or sell copies ;;; of the Software, and to permit persons to whom the Software is ;;; furnished to do so, subject to the following conditions: ;;; ;;; The above copyright notice and this permission notice shall be ;;; included in all copies or substantial portions of the Software. ;;; ;;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, ;;; EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF ;;; MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND ;;; NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT ;;; HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, ;;; WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ;;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER ;;; DEALINGS IN THE SOFTWARE. ;;; ;;; This implementation requires the Java Native Access (JNA) library. ;;; ;;; ;;; JNA may be automatically loaded into the current JVM process from ;;; abcl-1.1.0-dev via the contrib mechanism. (eval-when (:compile-toplevel :load-toplevel :execute) (require :abcl-contrib) (require :jna) (require :jss)) ;;; This is a preliminary version that will have to be cleaned up, ;;; optimized, etc. Nevertheless, it passes all of the relevant CFFI ;;; tests except MAKE-POINTER.HIGH. Shareable Vectors are not ;;; implemented yet. ;;;# Administrivia (defpackage #:cffi-sys (:use #:cl #:java) (:import-from #:alexandria #:hash-table-values #:length= #:format-symbol) (:export #:canonicalize-symbol-name-case #:foreign-pointer #:pointerp #:pointer-eq #:null-pointer #:null-pointer-p #:inc-pointer #:make-pointer #:pointer-address #:%foreign-alloc #:foreign-free #:with-foreign-pointer #:%foreign-funcall #:%foreign-funcall-pointer #:%foreign-type-alignment #:%foreign-type-size #:%load-foreign-library #:%close-foreign-library #:native-namestring #:%mem-ref #:%mem-set ;; #:make-shareable-byte-vector ;; #:with-pointer-to-vector-data #:%foreign-symbol-pointer #:%defcallback #:%callback #:with-pointer-to-vector-data #:make-shareable-byte-vector)) (in-package #:cffi-sys) ;;;# Loading and Closing Foreign Libraries (defparameter *loaded-libraries* (make-hash-table)) (defun %load-foreign-library (name path) "Load a foreign library, signals a simple error on failure." (flet ((load-and-register (name path) (let ((lib (jstatic "getInstance" "com.sun.jna.NativeLibrary" path))) (setf (gethash name *loaded-libraries*) lib) lib)) (foreign-library-type-p (type) (find type '("so" "dll" "dylib") :test #'string=)) (java-error (e) (error (jcall (jmethod "java.lang.Exception" "getMessage") (java-exception-cause e))))) (handler-case (load-and-register name path) (java-exception (e) ;; From JNA http://jna.java.net/javadoc/com/sun/jna/NativeLibrary.html ;; ``[The name] can be short form (e.g. "c"), an explicit ;; version (e.g. "libc.so.6"), or the full path to the library ;; (e.g. "/lib/libc.so.6")'' ;; ;; Try to deal with the occurance "libXXX" and "libXXX.so" as ;; "libXXX.so.6" and "XXX" should have succesfully loaded. (let ((p (pathname path))) (if (and (not (pathname-directory p)) (= (search "lib" (pathname-name p)) 0)) (let ((short-name (if (foreign-library-type-p (pathname-type p)) (subseq (pathname-name p) 3) (pathname-name p)))) (handler-case (load-and-register name short-name) (java-exception (e) (java-error e)))) (java-error e))))))) ;;; FIXME. Should remove libraries from the hash table. (defun %close-foreign-library (handle) "Closes a foreign library." #+#:ignore (setf *loaded-libraries* (remove handle *loaded-libraries*)) (jcall-raw (jmethod "com.sun.jna.NativeLibrary" "dispose") handle)) ;;; ;;; FIXME! We should probably define a private-jfield-accessor that does the hard work once! (let ((get-declared-fields-jmethod (jmethod "java.lang.Class" "getDeclaredFields"))) (defun private-jfield (class-name field-name instance) (let ((field (find field-name (jcall get-declared-fields-jmethod (jclass class-name)) :key #'jfield-name :test #'string=))) (jcall (jmethod "java.lang.reflect.Field" "setAccessible" "boolean") field +true+) (jcall (jmethod "java.lang.reflect.Field" "get" "java.lang.Object") field instance)))) ;;; XXX: doesn't match jmethod-arguments. (let ((get-declared-methods-jmethod (jmethod "java.lang.Class" "getDeclaredMethods"))) (defun private-jmethod (class-name method-name) (let ((method (find method-name (jcall get-declared-methods-jmethod (jclass class-name)) :key #'jmethod-name :test #'string=))) (jcall (jmethod "java.lang.reflect.Method" "setAccessible" "boolean") method +true+) method))) (let ((get-declared-constructors-jmethod (jmethod "java.lang.Class" "getDeclaredConstructors")) (set-accessible-jmethod (jmethod "java.lang.reflect.Constructor" "setAccessible" "boolean"))) (defun private-jconstructor (class-name &rest params) (let* ((param-classes (mapcar #'jclass params)) (cons (find-if (lambda (x &aux (cons-params (jconstructor-params x))) (and (length= param-classes cons-params) (loop for param in param-classes and param-x across cons-params always (string= (jclass-name param) (jclass-name param-x))))) (jcall get-declared-constructors-jmethod (jclass class-name))))) (jcall set-accessible-jmethod cons +true+) cons))) ;;;# Symbol Case (defun canonicalize-symbol-name-case (name) (string-upcase name)) ;;;# Pointers (deftype foreign-pointer () '(satisfies pointerp)) (defun pointerp (ptr) "Return true if PTR is a foreign pointer." (let ((jclass (jclass-of ptr))) (when jclass (jclass-superclass-p (jclass "com.sun.jna.Pointer") jclass)))) (let ((jconstructor (private-jconstructor "com.sun.jna.Pointer" "long"))) (defun make-pointer (address) "Return a pointer pointing to ADDRESS." (jnew jconstructor address))) (defun make-private-jfield-accessor (class-name field-name) (let ((field (find field-name (jcall (jmethod "java.lang.Class" "getDeclaredFields") (jclass class-name)) :key #'jfield-name :test #'string=))) (jcall (jmethod "java.lang.reflect.Field" "setAccessible" "boolean") field +true+) (let ((get-jmethod (jmethod "java.lang.reflect.Field" "get" "java.lang.Object"))) (lambda (instance) (jcall get-jmethod field instance))))) (let ((accessor (make-private-jfield-accessor "com.sun.jna.Pointer" "peer"))) (defun %pointer-address (pointer) (funcall accessor pointer))) (defun pointer-address (pointer) "Return the address pointed to by PTR." (let ((peer (%pointer-address pointer))) (if (< peer 0) (+ #.(ash 1 64) peer) peer))) (defun pointer-eq (ptr1 ptr2) "Return true if PTR1 and PTR2 point to the same address." (= (%pointer-address ptr1) (%pointer-address ptr2))) (defun null-pointer () "Construct and return a null pointer." (make-pointer 0)) (defun null-pointer-p (ptr) "Return true if PTR is a null pointer." (zerop (%pointer-address ptr))) (defun inc-pointer (ptr offset) "Return a fresh pointer pointing OFFSET bytes past PTR." (make-pointer (+ (%pointer-address ptr) offset))) ;;;# Allocation (let ((malloc-jmethod (private-jmethod "com.sun.jna.Memory" "malloc"))) (defun %foreign-alloc (size) "Allocate SIZE bytes on the heap and return a pointer." (make-pointer (jstatic-raw malloc-jmethod nil size)))) (let ((free-jmethod (private-jmethod "com.sun.jna.Memory" "free"))) (defun foreign-free (ptr) "Free a PTR allocated by FOREIGN-ALLOC." (jstatic-raw free-jmethod nil (%pointer-address ptr)) nil)) ;;; TODO: stack allocation. (defmacro with-foreign-pointer ((var size &optional size-var) &body body) "Bind VAR to SIZE bytes of foreign memory during BODY. The pointer in VAR is invalid beyond the dynamic extent of BODY, and may be stack-allocated if supported by the implementation. If SIZE-VAR is supplied, it will be bound to SIZE during BODY." (unless size-var (setf size-var (gensym "SIZE"))) `(let* ((,size-var ,size) (,var (%foreign-alloc ,size-var))) (unwind-protect (progn ,@body) (foreign-free ,var)))) ;;;# Shareable Vectors ;;; ;;; This interface is very experimental. WITH-POINTER-TO-VECTOR-DATA ;;; should be defined to perform a copy-in/copy-out if the Lisp ;;; implementation can't do this. (defun jna-setter (type) (ecase type ((:char :unsigned-char) "setByte") (:double "setDouble") (:float "setFloat") ((:int :unsigned-int) "setInt") ((:long :unsigned-long) "setNativeLong") ((:long-long :unsigned-long-long) "setLong") (:pointer "setPointer") ((:short :unsigned-short) "setShort"))) (defun jna-setter-arg-type (type) (ecase type ((:char :unsigned-char) "byte") (:double "double") (:float "float") ((:int :unsigned-int) "int") ((:long :unsigned-long) "com.sun.jna.NativeLong") ((:long-long :unsigned-long-long) "long") (:pointer "com.sun.jna.Pointer") ((:short :unsigned-short) "short"))) (defun jna-getter (type) (ecase type ((:char :unsigned-char) "getByte") (:double "getDouble") (:float "getFloat") ((:int :unsigned-int) "getInt") ((:long :unsigned-long) "getNativeLong") ((:long-long :unsigned-long-long) "getLong") (:pointer "getPointer") ((:short :unsigned-short) "getShort"))) (defun make-shareable-byte-vector (size) "Create a Lisp vector of SIZE bytes can passed to WITH-POINTER-TO-VECTOR-DATA." (make-array size :element-type '(unsigned-byte 8))) (let ((method (jmethod "com.sun.jna.Pointer" (jna-setter :char) "long" (jna-setter-arg-type :char)))) (defun copy-to-foreign-vector (vector foreign-pointer) (loop for i below (length vector) do (jcall-raw method foreign-pointer i (aref vector i))))) ;; hand-roll the jna-getter method instead of calling %mem-ref every time through (let ((method (jmethod "com.sun.jna.Pointer" (jna-getter :char) "long"))) (defun copy-from-foreign-vector (vector foreign-pointer) (loop for i below (length vector) do (setf (aref vector i) (java:jobject-lisp-value (jcall-raw method foreign-pointer i)))))) (defmacro with-pointer-to-vector-data ((ptr-var vector) &body body) "Bind PTR-VAR to a foreign pointer to the data in VECTOR." (let ((vector-sym (gensym "VECTOR"))) `(let ((,vector-sym ,vector)) (with-foreign-pointer (,ptr-var (length ,vector-sym)) (copy-to-foreign-vector ,vector-sym ,ptr-var) (unwind-protect (progn ,@body) (copy-from-foreign-vector ,vector-sym ,ptr-var)))))) ;;;# Dereferencing (defun foreign-type-to-java-class (type) (jclass (ecase type ((:int :unsigned-int) "java.lang.Integer") ((:long :unsigned-long) "com.sun.jna.NativeLong") ((:long-long :unsigned-long-long) "java.lang.Long") (:pointer "com.sun.jna.Pointer") ;; void * is pointer? (:float "java.lang.Float") (:double "java.lang.Double") ((:char :unsigned-char) "java.lang.Byte") ((:short :unsigned-short) "java.lang.Short")))) (defun %foreign-type-size (type) "Return the size in bytes of a foreign type." (jstatic "getNativeSize" "com.sun.jna.Native" (foreign-type-to-java-class type))) ;;; FIXME. (defun %foreign-type-alignment (type) "Return the alignment in bytes of a foreign type." (%foreign-type-size type)) (defun unsigned-type-p (type) (case type ((:unsigned-char :unsigned-int :unsigned-short :unsigned-long :unsigned-long-long) t) (t nil))) (defun lispify-value (value type) (when (and (eq type :pointer) (or (null (java:jobject-lisp-value value)) (eq +null+ (java:jobject-lisp-value value)))) (return-from lispify-value (null-pointer))) (when (or (eq type :long) (eq type :unsigned-long)) (setq value (jcall-raw (jmethod "com.sun.jna.NativeLong" "longValue") (java:jobject-lisp-value value)))) (let ((bit-size (* 8 (%foreign-type-size type)))) (let ((lisp-value (java:jobject-lisp-value value))) (if (and (unsigned-type-p type) (logbitp (1- bit-size) lisp-value)) (lognot (logxor lisp-value (1- (expt 2 bit-size)))) lisp-value)))) (defun %mem-ref (ptr type &optional (offset 0)) (lispify-value (jcall-raw (jmethod "com.sun.jna.Pointer" (jna-getter type) "long") ptr offset) type)) (defun %mem-set (value ptr type &optional (offset 0)) (let* ((bit-size (* 8 (%foreign-type-size type))) (val (if (and (unsigned-type-p type) (logbitp (1- bit-size) value)) (lognot (logxor value (1- (expt 2 bit-size)))) value))) (jcall-raw (jmethod "com.sun.jna.Pointer" (jna-setter type) "long" (jna-setter-arg-type type)) ptr offset (if (or (eq type :long) (eq type :unsigned-long)) (jnew (jconstructor "com.sun.jna.NativeLong" "long") val) val))) value) ;;;# Foreign Globals (let ((get-symbol-address-jmethod (private-jmethod "com.sun.jna.NativeLibrary" "getSymbolAddress"))) (defun %foreign-symbol-pointer (name library) "Returns a pointer to a foreign symbol NAME." (flet ((find-it (library) (ignore-errors (make-pointer (jcall-raw get-symbol-address-jmethod library name))))) (if (eq library :default) (or (find-it (jstatic "getProcess" "com.sun.jna.NativeLibrary")) ;; The above should find it, but I'm not exactly sure, so ;; let's still do it manually just in case. (loop for lib being the hash-values of *loaded-libraries* thereis (find-it lib))) (find-it library))))) ;;;# Calling Foreign Functions (defun find-foreign-function (name library) (flet ((find-it (library) (ignore-errors (jcall-raw (jmethod "com.sun.jna.NativeLibrary" "getFunction" "java.lang.String") library name)))) (if (eq library :default) (or (find-it (jstatic "getProcess" "com.sun.jna.NativeLibrary")) ;; The above should find it, but I'm not exactly sure, so ;; let's still do it manually just in case. (loop for lib being the hash-values of *loaded-libraries* thereis (find-it lib))) (find-it (gethash library *loaded-libraries*))))) (defun convert-calling-convention (convention) (ecase convention (:stdcall "ALT_CONVENTION") (:cdecl "C_CONVENTION"))) (defparameter *jna-string-encoding* "UTF-8" "Encoding for conversion between Java and native strings that occurs within JNA. Used with jna-4.0.0 or later.") ;;; c.f. (defvar *jna-4.0.0-or-later-p* (ignore-errors (private-jconstructor "com.sun.jna.Function" "com.sun.jna.Pointer" "int" "java.lang.String"))) (let ((jconstructor (if *jna-4.0.0-or-later-p* (private-jconstructor "com.sun.jna.Function" "com.sun.jna.Pointer" "int" "java.lang.String") (private-jconstructor "com.sun.jna.Function" "com.sun.jna.Pointer" "int")))) (defun make-function-pointer (pointer convention) (apply #'jnew jconstructor pointer (jfield "com.sun.jna.Function" (convert-calling-convention convention)) (when *jna-4.0.0-or-later-p* (list *jna-string-encoding*))))) (defun lisp-value-to-java (value foreign-type) (case foreign-type (:pointer value) (:void nil) (t (jnew (ecase foreign-type ((:int :unsigned-int) (jconstructor "java.lang.Integer" "int")) ((:long-long :unsigned-long-long) (jconstructor "java.lang.Long" "long")) ((:long :unsigned-long) (jconstructor "com.sun.jna.NativeLong" "long")) ((:short :unsigned-short) (jconstructor "java.lang.Short" "short")) ((:char :unsigned-char) (jconstructor "java.lang.Byte" "byte")) (:float (jconstructor "java.lang.Float" "float")) (:double (jconstructor "java.lang.Double" "double"))) value)))) (defun %%foreign-funcall (function args arg-types return-type) (let ((jargs (jnew-array "java.lang.Object" (length args)))) (loop for arg in args and type in arg-types and i from 0 do (setf (jarray-ref jargs i) (lisp-value-to-java arg type))) (if (eq return-type :void) (progn (jcall-raw (jmethod "com.sun.jna.Function" "invoke" "[Ljava.lang.Object;") function jargs) (values)) (lispify-value (jcall-raw (jmethod "com.sun.jna.Function" "invoke" "java.lang.Class" "[Ljava.lang.Object;") function (foreign-type-to-java-class return-type) jargs) return-type)))) (defun foreign-funcall-type-and-args (args) (let ((return-type :void)) (loop for (type arg) on args by #'cddr if arg collect type into types and collect arg into fargs else do (setf return-type type) finally (return (values types fargs return-type))))) (defmacro %foreign-funcall (name args &key library convention) (declare (ignore convention)) (multiple-value-bind (types fargs rettype) (foreign-funcall-type-and-args args) `(%%foreign-funcall (find-foreign-function ',name ',library) (list ,@fargs) ',types ',rettype))) (defmacro %foreign-funcall-pointer (ptr args &key convention) (multiple-value-bind (types fargs rettype) (foreign-funcall-type-and-args args) `(%%foreign-funcall (make-function-pointer ,ptr ',convention) (list ,@fargs) ',types ',rettype))) ;;;# Callbacks (defun foreign-to-callback-type (type) (ecase type ((:int :unsigned-int) :int) ((:long :unsigned-long) (jvm::make-jvm-class-name "com.sun.jna.NativeLong")) ((:long-long :unsigned-long-long) (jvm::make-jvm-class-name "java.lang.Long")) (:pointer (jvm::make-jvm-class-name "com.sun.jna.Pointer")) (:float :float) (:double :double) ((:char :unsigned-char) :byte) ((:short :unsigned-short) :short) (:wchar_t :int) (:void :void))) (defvar *callbacks* (make-hash-table)) (defmacro convert-args-to-lisp-values (arg-names arg-types &body body) (let ((gensym-args (loop for name in arg-names collect (format-symbol t '#:callback-arg-~a- name)))) `(lambda (,@gensym-args) (let ,(loop for arg in arg-names for type in arg-types for gensym-arg in gensym-args collecting `(,arg (if (typep ,gensym-arg 'java:java-object) (lispify-value ,gensym-arg ,type) ,gensym-arg))) ,@body)))) (defmacro %defcallback (name return-type arg-names arg-types body &key convention) (declare (ignore convention)) ;; I'm always up for ignoring convention, but this is probably wrong. `(setf (gethash ',name *callbacks*) (jinterface-implementation (ensure-callback-interface ',return-type ',arg-types) "callback" (convert-args-to-lisp-values ,arg-names ,arg-types (lisp-value-to-java ,body ',return-type))))) ;; (lambda (,@arg-names) ,body)))) (jvm::define-class-name +callback-object+ "com.sun.jna.Callback") (defconstant +dynamic-callback-package+ "org/armedbear/jna/dynamic/callbacks" "The slash-delimited Java package in which we create classes dynamically to specify callback interfaces.") (defun ensure-callback-interface (returns args) "Ensure that the jvm interface for the callback exists in the current JVM. Returns the fully dot qualified name of the interface." (let* ((jvm-returns (foreign-to-callback-type returns)) (jvm-args (mapcar #'foreign-to-callback-type args)) (interface-name (qualified-callback-interface-classname jvm-returns jvm-args))) (handler-case (jss:find-java-class interface-name) (java-exception (e) (when (jinstance-of-p (java:java-exception-cause e) "java.lang.ClassNotFoundException") (let ((interface-class-bytes (%define-jna-callback-interface jvm-returns jvm-args)) (simple-interface-name (callback-interface-classname jvm-returns jvm-args))) (load-class interface-name interface-class-bytes))))) interface-name)) (defun qualified-callback-interface-classname (returns args) (format nil "~A.~A" (substitute #\. #\/ +dynamic-callback-package+) (callback-interface-classname returns args))) (defun callback-interface-classname (returns args) (flet ((stringify (thing) (typecase thing (jvm::jvm-class-name (substitute #\_ #\/ (jvm::class-name-internal thing))) (t (string thing))))) (format nil "~A__~{~A~^__~}" (stringify returns) (mapcar #'stringify args)))) (defun %define-jna-callback-interface (returns args) "Returns the Java byte[] array of a class representing a Java interface descending form +CALLBACK-OBJECT+ which contains the single function 'callback' which takes ARGS returning RETURNS. The fully qualified dotted name of the generated class is returned as the second value." (let ((name (callback-interface-classname returns args))) (values (define-java-interface name +dynamic-callback-package+ `(("callback" ,returns ,args)) `(,+callback-object+)) (qualified-callback-interface-classname returns args)))) (defun define-java-interface (name package methods &optional (superinterfaces nil)) "Returns the bytes of the Java class interface called NAME in PACKAGE with METHODS. METHODS is a list of (NAME RETURN-TYPE (ARG-TYPES)) entries. NAME is a string. The values of RETURN-TYPE and the list of ARG-TYPES for the defined method follow the are either references to Java objects as created by JVM::MAKE-JVM-CLASS-NAME, or keywords representing Java primtive types as contained in JVM::MAP-PRIMITIVE-TYPE. SUPERINTERFACES optionally contains a list of interfaces that this interface extends specified as fully qualifed dotted Java names." (let* ((class-name-string (format nil "~A/~A" package name)) (class-name (jvm::make-jvm-class-name class-name-string)) (class (jvm::make-class-interface-file class-name))) (dolist (superinterface superinterfaces) (jvm::class-add-superinterface class (if (typep superinterface 'jvm::jvm-class-name) superinterface (jvm::make-jvm-class-name superinterface)))) (dolist (method methods) (let ((name (first method)) (returns (second method)) (args (third method))) (jvm::class-add-method class (jvm::make-jvm-method name returns args :flags '(:public :abstract))))) (jvm::finalize-class-file class) (let ((s (sys::%make-byte-array-output-stream))) (jvm::write-class-file class s) (sys::%get-output-stream-bytes s)))) (defun load-class (name bytes) "Load the byte[] array BYTES as a Java class called NAME." (#"loadClassFromByteArray" java::*classloader* name bytes)) ;;; Test function: unused in CFFI (defun write-class (class-bytes pathname) "Write the Java byte[] array CLASS-BYTES to PATHNAME." (with-open-file (stream pathname :direction :output :element-type '(signed-byte 8)) (dotimes (i (jarray-length class-bytes)) (write-byte (jarray-ref class-bytes i) stream)))) (defun %callback (name) (or (#"getFunctionPointer" 'com.sun.jna.CallbackReference (gethash name *callbacks*)) (error "Undefined callback: ~S" name))) (defun native-namestring (pathname) (namestring pathname))