;;;; -*- Mode: lisp; indent-tabs-mode: nil -*-
;;;
;;; cffi-abcl.lisp --- CFFI-SYS implementation for ABCL/JNA.
;;;
;;; Copyright (C) 2009, Luis Oliveira  <loliveira@common-lisp.net>
;;; Copyright (C) 2012, Mark Evenson  <evenson.not.org@gmail.com>
;;;
;;; 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.
;;; <http://jna.dev.java.net/>
;;;
;;; 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
   #:%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")))

;;; HACK for now: keep track of all the pointers to malloc()'d memory
;;; hashed by the shareable byte vectors we allocate.
(defvar *static-vector-pointer*
  (make-hash-table :weakness :value))

;;; available in abcl-1.7.0
;;; presence of :nio-buffer keyword to CL:MAKE-ARRAY
#+nio
(defun make-shareable-vector (length &key (element-type '(unsigned-byte 8)))
  "Use memory on the heap for storing a vector of LENGTH with ELEMENT-TYPE

Returns the allocated vector as the first value, and the pointer to
the heap memory as the second.

Only works for 8, 16, 32 bit bytes.
"
  (let* ((type
           (first element-type))
         (bits-per-byte
           (second element-type))
         (bytes-per-element  ;; ehh, not going to work well for element type not of size 8, 16, or 32
           (ceiling bits-per-byte 8)))
    (unless (subtypep element-type
                      '(or (unsigned-byte 8) (unsigned-byte 16) (unsigned-byte 32)))
      (signal 'type-error :datum element-type
                          :expected-type '(or
                                           (unsigned-byte 8)
                                           (unsigned-byte 16)
                                           (unsigned-byte 32))))
    (let* ((bytes
             (* length bytes-per-element))
           (heap-pointer
             (jss:new "com.sun.jna.Memory" bytes))
           (bytebuffer
             (#"getByteBuffer" heap-pointer 0 bytes))
           (static-vector
             (make-array length :element-type element-type :nio-buffer bytebuffer)))
      (setf (gethash static-vector *static-vector-pointer*)
            heap-pointer)
      (values
       static-vector
       heap-pointer))))

(defun make-shareable-byte-vector (size)
  "Create a Lisp vector of SIZE bytes can passed to
WITH-POINTER-TO-VECTOR-DATA."
  (if (find :nio *features*)
      (make-shareable-vector size :element-type '(unsigned-byte 8)) ;; abcl-1.6.2-dev, upcoming abcl-1.7.0
      (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"))
        (heap-pointer (gethash vector *static-vector-pointer*)))
    (if heap-pointer
        `(let ((,ptr-var ,heap-pointer))
           (progn ,@body))
        `(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. <http://twall.github.io/jna/4.0/javadoc/com/sun/jna/Function.html#Function%28com.sun.jna.Pointer,%20int,%20java.lang.String%29>
(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 :default) 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))
