Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/bohonghuang/cffi-object

A Common Lisp library that enables fast and convenient interoperation with foreign objects.
https://github.com/bohonghuang/cffi-object

Last synced: 3 months ago
JSON representation

A Common Lisp library that enables fast and convenient interoperation with foreign objects.

Awesome Lists containing this project

README

        

#+TITLE: cffi-object
Fast and convenient foreign object interoperation via CFFI.
* Introduction
When developing projects that heavily use CFFI, interfacing with foreign libraries and managing memory are unavoidable issues.
The following are three commonly used approaches:
1. Expose raw pointers to the high level code. \\
This approach is very lightweight and efficient, but it requires programmers to manually manage memory.
Even if macros that expand to ~unwind-protect~ can be used to manage resources with dynamic extent,
it can sometimes make the code style unnatural under some scenarios that don't require deterministic time for resource acquisition and release.
2. Provide high-level wrapper classes or structs (all the fields are of Lisp's native types) with ~cffi:translate-from/to-foreign~ or ~cffi:expand-from/to-foreign~ defined. \\
This hands over the memory management to Lisp's GC and makes it more natural when operating data received from or passed to the foreign.
However, for foreign functions that directly accept structs by value, it requires ~cffi-libffi~, which [[https://www.reddit.com/r/lisp/comments/ygebes/passing_c_struct_by_value_cffilibffi_is_250x/][has significant overhead]] under frequent invocations.
CFFI does not automatically call the translation mechanism mentioned above for foreign functions that accept struct pointers as parameters,
so users or library developers need to first allocate memory on the stack using ~with-foreign-object(s)~ (if the Lisp implementation does not support it, it may be allocated on the heap),
and CFFI will perform the conversion with ~cffi:translate-from/to-foreign~ at runtime or ~cffi:expand-from/to-foreign~ at compile-time.
The overhead involved in this process is not negligible for large structs, especially for real-time media processing, gaming, and other CPU intensive applications.
3. Define structs for each CFFI type, wrap a pointer inside, and selectively use ~trivial-garbage~ to manage the memory. \\
This approach seems to combine the advantages of the above two methods. In most cases, programmers don't need to concern themselves with memory.
Except for making the timing of resource release uncertain and putting some potential pressure on the GC,
it has good performance because many implementations (such as SBCL and ECL) operate foreign memory efficiently.
Additionally, this approach does not have the overhead brought by ~cffi:translate-from/to-foreign~ or ~cffi:expand-from/to-foreign~,
making it ideal for applications that require frequent calls to foreign functions,
such as calling foreign functions for SIMD-accelerated matrix calculations or outputting audio buffers to audio devices.

~cffi-object~ adopts the third approach above and provides a uniform way to directly convert existing CFFI type definitions (which can be generated by autowrapping tools like [[https://github.com/borodust/claw][claw]])
into Lisp's struct and function definitions, allowing you to operate on foreign data types as if they were native types in Lisp, without having to write glue code by hand.

~cffi-object~ should run on any implementation that supports [[https://github.com/cffi/cffi][CFFI]] and [[https://github.com/trivial-garbage/trivial-garbage][trivial-garbage]].
To test the system, simply eval ~(asdf:test-system :cffi-object)~ in the REPL.
* Features
- *Generate CLOS classes for foreign types and use them as if they are native Lisp types* \\
You can generate the structure definition for a existing CFFI type:

#+BEGIN_SRC lisp
(cffi:defcstruct vector2
(x :float)
(y :float))

(cobj:define-cobject-class (vector2 (:struct vector2)))
#+END_SRC

Or you can generate structure definitions for all the CFFI types declared in a package.
This can be useful if you have an existing library that already defined those CFFI types:

#+BEGIN_SRC lisp
(cl:defpackage #:mylib
(:use #:cl))

(cl:in-package #:mylib)

(cffi:defcstruct vector2
(x :float)
(y :float))

(cffi:defcstruct camera-2d
(offset (:struct vector2))
(target (:struct vector2))
(rotation :float)
(zoom :float))

(cobj:define-cobject-class #:mylib)
#+END_SRC

Then you can create or modify objects of these types just like using structs defined with ~defstruct~:

#+BEGIN_SRC lisp
MYLIB> (make-vector2)
#
MYLIB> ; The memory is unintialized by default
; No values
MYLIB> (make-vector2 :x 1.0 :y 2.0)
#
MYLIB> (make-camera-2d :offset * :target * :rotation 0.0 :zoom 1.0)
#
:TARGET #
:ROTATION 0.0
:ZOOM 1.0
@0x00007F3C840011B0>
MYLIB> (camera-2d-offset *)
#
MYLIB> (copy-vector2 *)
#
MYLIB> (setf (vector2-x *) 2.0)
2.0
MYLIB> (copy-vector2 ** ***) ; In-place copy
#
MYLIB> (vector2-equal * ***)
T
#+END_SRC

You can also define generic methods specialized for these foreign types:

#+BEGIN_SRC lisp
MYLIB> (defmethod position2 ((camera camera-2d)) (camera-2d-offset camera))
#
MYLIB> (defmethod position2 ((vector vector2)) vector)
#
MYLIB> (position2 (make-camera-2d))
#
MYLIB> (position2 (make-vector2))
#
#+END_SRC
- *Low overhead when interfacing with foreign functions* \\
All the objects created with ~cffi-object~ are fixed in memory and have the same memory representation as C,
which means that structures can be passed directly to C functions or objects can be created directly
by returning a pointer to a structure from a C function without conversion needed.

#+BEGIN_SRC lisp
(cl:in-package #:mylib)

(declaim (inline vector2-add))
(cffi:defcfun ("__claw_Vector2Add" vector2-add) (:pointer (:struct vector2))
(%%claw-result- (:pointer (:struct vector2)))
(v1 (:pointer (:struct vector2)))
(v2 (:pointer (:struct vector2))))

(let ((v1 (make-vector2 :x 1.0 :y 2.0))
(v2 (make-vector2 :x 3.0 :y 4.0)))
(vector2-add (cobj:cobject-pointer v1)
(cobj:cobject-pointer v1)
(cobj:cobject-pointer v2))
v1) ; => #
#+END_SRC
- *Automatic and safe memory management* \\
All objects created by Lisp are automatically managed by the GC (Garbage Collector),
and any reference to an object or its fields will prevent the memory of that object from being released:

#+BEGIN_SRC lisp
(let* ((cam (make-camera-2d))
(vec (camera-2d-offset cam)))
;; VEC is a reference to the OFFSET field of CAMERA-2D,
;; which will share memory in a certain region.
vec) ; => #
;; This is safe because VEC holds a reference to CAM,
;; which will prevent both GC from collecting CAM and
;; releasing the corresponding memory.
#+END_SRC

Exchanging object ownership with C functions is convenient:

#+BEGIN_SRC lisp
(cl:in-package #:mylib)

(declaim (inline malloc))
(cffi:defcfun malloc :pointer ; cffi:foreign-alloc
(size :size))

(declaim (inline free))
(cffi:defcfun free :void ; cffi:foreign-free
(size :pointer))

(let* ((vec1 (cobj:manage-cobject ; Take ownership of the object from foreign and responsible for freeing the memory.
(cobj:pointer-cobject
(malloc (cffi:foreign-type-size
'(:struct vector2)))
'vector2)))
(vec2 (cobj:pointer-cobject ; Share the memory of this object with foreign and not responsible for freeing the memory.
(cobj:cobject-pointer vec1)
'vector2)))
(assert (vector2-equal vec1 vec2))
(free (cobj:unmanage-cobject vec1))) ; Transfer ownership of the object to foreign and no longer responsible for freeing its memory.
#+END_SRC

But when you transfer the deallocation of memory to foreign code, you should be aware that the memory of this object may become invalid at any time
if it is deallocated by the foreign.
- *Bring unboxed struct/array and by-value assignment to Common Lisp* \\
~cffi-object~ is capable of creating unboxed structs or arrays, which are fully compatible with C,
so pointers can be directly passed to foreign:

#+BEGIN_SRC lisp
(cl:in-package #:mylib)

(cffi:defcstruct named-vector2-buffer
(name :string)
(buffer (:array (:struct vector2) 64))
(size :size))

(cobj:define-cobject-class (:struct named-vector2-buffer))
#+END_SRC

#+BEGIN_SRC lisp
MYLIB> (cffi:foreign-type-size '(:struct named-vector2-buffer))
528
MYLIB> (make-named-vector2-buffer :name "DEFAULT" :size 0)
#
#
#
#
#
#
#
#
#
# ... [54 elements elided]>
:SIZE 0
@0x00007F3C8400FCC0>
MYLIB> (cobj:cfill (named-vector2-buffer-buffer *) (make-vector2 :x 1.0 :y 2.0))
#<#
#
#
#
#
#
#
#
#
# ... [54 elements elided]>
MYLIB> (cobj:make-carray 5 :element-type 'vector2
:initial-contents (loop :for i :below 5
:collect (make-vector2 :x (coerce i 'single-float)
:y (coerce i 'single-float))))
#<#
#
#
#
#>
MYLIB> (cobj:creplace ** *)
#<#
#
#
#
#
#
#
#
#
# ... [54 elements elided]>
#+END_SRC
* Related Projects
- [[https://github.com/digikar99/unboxables][unboxables]] \\
~unboxables~ can provide unboxed struct/array features for Common Lisp too,
and it uses a more compact memory layout, which can potentially have lower memory consumption,
while ~cffi-object~ , by default, uses the C memory representation which may have padding between fields,
allowing you to pass pointers to foreign functions directly.
Currently, ~cffi-cobject~ may not have the high-performance array operations that ~unboxables~ provides.
It is more focused on interoperation with foreign anyway.
- [[https://github.com/bohonghuang/cffi-ops][cffi-ops]] \\
~cffi-ops~ provides some macros expanded at compile-time, so it doesn't cons and can be used in performance-sensitive functions,
which allows you to implement GC-free and high performance algorithms.
System ~cffi-object.ops~ provides ~cffi-object~ the integration with ~cffi-ops~, which can be enabled by ~(cobj.ops:enable-cobject-ops)~ at compile-time:

#+BEGIN_SRC lisp
(cl:in-package #:mylib)

(eval-when (:compile-toplevel :load-toplevel :execute)
(cobj.ops:enable-cobject-ops))

(let ((vec1 (make-vector2 :x 1.0 :y 2.0))
(vec2 (make-vector2 :x 3.0 :y 4.0)))
(clocally (declare (ctype (:object (:struct vector2)) vec1 vec2))
(vector2-add (& vec1) (& vec1) (& vec2))
(assert (= (-> vec1 x) 4.0))
(assert (= (-> (& vec1) y) 6.0))))
#+END_SRC