Separate memory ops into their own ns

This commit is contained in:
Joshua Suskalo 2021-09-26 14:11:29 -05:00
parent c90f0e0a18
commit 5f96439432
4 changed files with 807 additions and 768 deletions

120
README.md
View file

@ -42,11 +42,12 @@ In the simplest cases, the native functions you call will work exclusively with
built-in types, for example the function `strlen` from libc.
```clojure
(require '[coffi.ffi :as ffi :refer [defcfn defalias]])
(require '[coffi.mem :as mem :refer [defalias]])
(require '[coffi.ffi :as ffi :refer [defcfn]])
(defcfn strlen
"Given a string, measures its length in bytes."
strlen [::ffi/c-string] ::ffi/long)
strlen [::mem/c-string] ::mem/long)
(strlen "hello")
;; => 5
@ -61,7 +62,7 @@ If you wish to use a native function as an anonymous function, it can be done
with the `cfn` function.
```clojure
((ffi/cfn "strlen" [::ffi/c-string] ::ffi/long) "hello")
((ffi/cfn "strlen" [::mem/c-string] ::mem/long) "hello")
;; => 5
```
@ -134,7 +135,7 @@ The corresponding coffi definition is like so:
```clojure
(defcfn zero-point
"zero" [] [::ffi/struct [[:x ::ffi/float] [:y ::ffi/float]]])
"zero" [] [::mem/struct [[:x ::mem/float] [:y ::mem/float]]])
(zero-point)
;; => {:x 0.0,
@ -146,9 +147,9 @@ macro `defalias` is used to define a struct alias.
```clojure
(defalias ::point
[::ffi/struct
[[:x ::ffi/float]
[:y ::ffi/float]]])
[::mem/struct
[[:x ::mem/float]
[:y ::mem/float]]])
(defcfn zero-point
"zero" [] ::point)
@ -159,7 +160,7 @@ native function, but dosn't need to be read back in, the `pointer` primitive
type can take a type argument.
```clojure
[::ffi/pointer ::ffi/int]
[::mem/pointer ::mem/int]
```
Arrays are also supported via a type argument. Keep in mind that they are the
@ -167,7 +168,7 @@ array itself, and not a pointer to the array like you might see in certain cases
in C.
```clojure
[::ffi/array ::ffi/int 3]
[::mem/array ::mem/int 3]
```
### Callbacks
@ -175,7 +176,7 @@ In addition to these composite types, there is also support for Clojure
functions.
```clojure
[::ffi/fn [::ffi/c-string] ::ffi/int]
[::ffi/fn [::mem/c-string] ::mem/int]
```
Be aware though that if an exception is thrown out of a callback that is called
@ -189,14 +190,14 @@ Some native functions can take any number of arguments, and in these cases coffi
provides `vacfn-factory` (for "varargs C function factory").
```clojure
(def printf-factory (ffi/vacfn-factory "printf" [::ffi/c-string] ::ffi/int))
(def printf-factory (ffi/vacfn-factory "printf" [::mem/c-string] ::mem/int))
```
This returns a function of the types of the rest of the arguments which itself
returns a native function wrapper.
```clojure
(def print-int (printf-factory ::ffi/int))
(def print-int (printf-factory ::mem/int))
(print-int "Some integer: %d\n" 5)
;; Some integer: 5
@ -213,7 +214,7 @@ Some libraries include global variables or constants accessible through symbols.
To start with, constant values stored in symbols can be fetched with `const`
```clojure
(def some-const (ffi/const "some_const" ::ffi/int))
(def some-const (ffi/const "some_const" ::mem/int))
```
This value is fetched once when you call `const` and is turned into a Clojure
@ -221,7 +222,7 @@ value. If you need to refer to a global variable, then `static-variable` can be
used to create a reference to the native value.
```clojure
(def some-var (ffi/static-variable "some_var" ::ffi/int))
(def some-var (ffi/static-variable "some_var" ::mem/int))
```
This variable is an `IDeref`. Each time you dereference it, the value will be
@ -249,12 +250,12 @@ Clojure code to make this easier.
```clojure
(defcfn takes-array
"takes_array_with_count" [::ffi/pointer ::ffi/long] ::ffi/void
"takes_array_with_count" [::mem/pointer ::mem/long] ::mem/void
native-fn
[ints]
(let [arr-len (count ints)
int-array (serialize ints [::ffi/array ::ffi/int arr-len]
(native-fn (ffi/address-of int-array) arr-len))]))
int-array (serialize ints [::mem/array ::mem/int arr-len]
(native-fn (mem/address-of int-array) arr-len))]))
```
The symbol `native-fn` can be any unqualified symbol, and names the native
@ -268,17 +269,17 @@ This can be used to implement out variables often seen in native code.
```clojure
(defcfn out-int
"out_int" [::ffi/pointer] ::ffi/void
"out_int" [::mem/pointer] ::mem/void
native-fn
[i]
(let [int-ptr (serialize i [::ffi/pointer ::ffi/int])]
(let [int-ptr (serialize i [::mem/pointer ::mem/int])]
(native-fn int-ptr)
(deserialize int-ptr [::ffi/pointer ::ffi/int])))
(deserialize int-ptr [::mem/pointer ::mem/int])))
```
### Scopes
In order to serialize any non-primitive type (such as the previous
`[::ffi/pointer ::ffi/int]`), off-heap memory needs to be allocated. When memory
`[::mem/pointer ::mem/int]`), off-heap memory needs to be allocated. When memory
is allocated inside the JVM, the memory is associated with a scope. Because none
was provided here, the scope is an implicit scope, and the memory will be freed
when the serialized object is garbage collected.
@ -294,13 +295,13 @@ stack scope.
```clojure
(defcfn out-int
"out_int" [::ffi/pointer] ::ffi/void
"out_int" [::mem/pointer] ::mem/void
native-fn
[i]
(with-open [scope (ffi/stack-scope)]
(let [int-ptr (ffi/serialize i [::ffi/pointer ::ffi/int] scope)]
(with-open [scope (mem/stack-scope)]
(let [int-ptr (mem/serialize i [::mem/pointer ::mem/int] scope)]
(native-fn int-ptr)
(ffi/deserialize int-ptr [::ffi/pointer ::ffi/int]))))
(mem/deserialize int-ptr [::mem/pointer ::mem/int]))))
```
This will free the pointer immediately upon leaving the function.
@ -330,9 +331,9 @@ The multimethod `primitive-type` returns the primitive type that a given type
serializes to. For this example, it should be a pointer.
```clojure
(defmethod ffi/primitive-type ::vector
(defmethod mem/primitive-type ::vector
[_type]
::ffi/pointer)
::mem/pointer)
```
For any type which doesn't serialize to a primitive, it returns nil, and
@ -342,14 +343,14 @@ Next is `serialize*` and `deserialize*`, multimethods that work with types that
serialize to primitives.
```clojure
(defmethod ffi/serialize* ::vector
(defmethod mem/serialize* ::vector
[obj _type scope]
(ffi/address-of (ffi/serialize obj [::ffi/array ::ffi/float 3] scope)))
(mem/address-of (mem/serialize obj [::mem/array ::mem/float 3] scope)))
(defmethod ffi/deserialize* ::vector
(defmethod mem/deserialize* ::vector
[addr _type]
(ffi/deserialize (ffi/slice-global addr (ffi/size-of [::ffi/array ::ffi/float 3]))
[::ffi/array ::ffi/float 3]))
(mem/deserialize (mem/slice-global addr (mem/size-of [::mem/array ::mem/float 3]))
[::mem/array ::mem/float 3]))
```
The `slice-global` function allows you to take an address without an associated
@ -361,7 +362,7 @@ function that takes a pointer exists, we could use this:
```clojure
(defcfn returns-vector
"returns_vector" [] ::ffi/pointer
"returns_vector" [] ::mem/pointer
native-fn
[scope]
(let [ret-ptr (native-fn)]
@ -387,7 +388,7 @@ To represent this, we can have a `tagged-union` type. For this instance of the
result type, it may look like this:
```clojure
[::tagged-union [:ok :err] {:ok ::ffi/int :err ::ffi/c-string}]
[::tagged-union [:ok :err] {:ok ::mem/int :err ::mem/c-string}]
```
The native representation of these objects is a struct of the tag and a union of
@ -396,11 +397,11 @@ we need a representation of the native layout of the data. The `c-layout`
multimethod provides that.
```clojure
(defmethod ffi/c-layout ::tagged-union
(defmethod mem/c-layout ::tagged-union
[[_tagged-union tags type-map]]
(ffi/c-layout [::ffi/struct
[[:tag ::ffi/long]
[:value [::ffi/union (vals type-map)]]]]))
(mem/c-layout [::mem/struct
[[:tag ::mem/long]
[:value [::mem/union (vals type-map)]]]]))
```
Types with type arguments are represented as vectors of the type name and any
@ -421,13 +422,13 @@ deserialize the value into and out of memory segments. This is accomplished with
(filter (comp #{item} second))
(map first))))
(defmethod ffi/serialize-into ::tagged-union
(defmethod mem/serialize-into ::tagged-union
[obj [_tagged-union tags type-map] segment scope]
(ffi/serialize-into
(mem/serialize-into
{:tag (item-index tags (first obj))
:value (second obj)}
[::ffi/struct
[[:tag ::ffi/long]
[::mem/struct
[[:tag ::mem/long]
[:value (get type-map (first obj))]]]
segment
scope))
@ -438,12 +439,12 @@ a map, and serializes it as a struct, choosing the type of the value based on
the tag.
```clojure
(defmethod ffi/deserialize-from ::tagged-union
(defmethod mem/deserialize-from ::tagged-union
[segment [_tagged-union tags type-map]]
(let [tag (ffi/deserialize-from segment ::ffi/long)]
(let [tag (mem/deserialize-from segment ::mem/long)]
[(nth tags tag)
(ffi/deserialize-from
(ffi/slice segment (ffi/size-of ::ffi/long))
(mem/deserialize-from
(mem/slice segment (mem/size-of ::mem/long))
(get type-map tag))]))
```
@ -459,11 +460,11 @@ is rather limited. It can be serialized, but not deserialized without external
information.
```clojure
[::ffi/union
#{::ffi/float ::ffi/double}
[::mem/union
#{::mem/float ::mem/double}
:dispatch #(cond
(float? %) ::ffi/float
(double? %) ::ffi/double)]
(float? %) ::mem/float
(double? %) ::mem/double)]
```
This is a minimal union in coffi. If the `:dispatch` keyword argument is not
@ -476,11 +477,11 @@ may also be provided. In the case of the value in the tagged union from before,
it could be represented for serialization purposes like so:
```clojure
[::ffi/union
#{::ffi/int ::ffi/c-string}
[::mem/union
#{::mem/int ::mem/c-string}
:dispatch #(case (first %)
:ok ::ffi/int
:err ::ffi/c-string)
:ok ::mem/int
:err ::mem/c-string)
:extract second]
```
@ -501,8 +502,8 @@ The functions `make-downcall` and `make-varargs-factory` are provided to create
these raw handles.
```clojure
(def raw-strlen (ffi/make-downcall "strlen" [::ffi/c-string] ::ffi/long))
(raw-strlen (ffi/serialize "hello" ::ffi/c-string))
(def raw-strlen (ffi/make-downcall "strlen" [::mem/c-string] ::mem/long))
(raw-strlen (mem/serialize "hello" ::mem/c-string))
;; => 5
```
@ -521,7 +522,7 @@ In addition, function types can be specified as being raw, in the following
manner:
```clojure
[::ffi/fn [::ffi/int] ::ffi/int :raw-fn? true]
[::ffi/fn [::mem/int] ::mem/int :raw-fn? true]
```
Clojure functions serialized to this type will have their arguments and return
@ -541,8 +542,8 @@ The data to represent an API is a map with the following form:
(def strlen-libspec
{:strlen {:type :function
:symbol "strlen"
:function/args [::ffi/c-string]
:function/ret ::ffi/long}})
:function/args [::mem/c-string]
:function/ret ::mem/long}})
```
Each key in this map represents a single symbol to be loaded. The value is a map
@ -585,6 +586,7 @@ These features are planned for future releases.
- Support for va_args type
- Functions for wrapping structs in padding following various standards
- Header parsing tool for generating a data model?
- Generic type aliases
## License

View file

@ -5,6 +5,7 @@
(:require
[clojure.java.io :as io]
[clojure.spec.alpha :as s]
[coffi.mem :as mem]
[insn.core :as insn])
(:import
(clojure.lang
@ -18,589 +19,9 @@
Addressable
CLinker
FunctionDescriptor
MemoryAccess
MemoryAddress
MemoryLayout
MemorySegment
ResourceScope
SegmentAllocator)))
(defn stack-scope
"Constructs a new scope for use only in this thread.
The memory allocated within this scope is cheap to allocate, like a native
stack."
^ResourceScope []
(ResourceScope/newConfinedScope))
(defn shared-scope
"Constructs a new shared scope.
This scope can be shared across threads and memory allocated in it will only
be cleaned up once every thread accessing the scope closes it."
^ResourceScope []
(ResourceScope/newSharedScope))
(defn connected-scope
"Constructs a new scope to reclaim all connected resources at once.
The scope may be shared across threads, and all resources created with it will
be cleaned up at the same time, when all references have been collected.
This type of scope cannot be closed, and therefore should not be created in
a [[with-open]] clause."
^ResourceScope []
(ResourceScope/newImplicitScope))
(defn global-scope
"Constructs the global scope, which will never reclaim its resources.
This scope may be shared across threads, but is intended mainly in cases where
memory is allocated with [[alloc]] but is either never freed or whose
management is relinquished to a native library, such as when returned from a
callback."
^ResourceScope []
(ResourceScope/globalScope))
(defn scope-allocator
"Constructs a segment allocator from the given `scope`.
This is primarily used when working with unwrapped downcall functions. When a
downcall function returns a non-primitive type, it must be provided with an
allocator."
^SegmentAllocator [^ResourceScope scope]
(SegmentAllocator/ofScope scope))
(defn alloc
"Allocates `size` bytes.
If a `scope` is provided, the allocation will be reclaimed when it is closed."
([size] (alloc size (connected-scope)))
([size scope] (MemorySegment/allocateNative (long size) ^ResourceScope scope)))
(defn alloc-with
"Allocates `size` bytes using the `allocator`."
([allocator size]
(.allocate ^SegmentAllocator allocator (long size)))
([allocator size alignment]
(.allocate ^SegmentAllocator allocator (long size) (long alignment))))
(defmacro with-acquired
"Acquires a `scope` to ensure it will not be released until the `body` completes.
This is only necessary to do on shared scopes, however if you are operating on
an arbitrary passed scope, it is best practice to wrap code that interacts
with it wrapped in this."
[scope & body]
`(let [scope# ~scope
handle# (.acquire ^ResourceScope scope#)]
(try ~@body
(finally (.release ^ResourceScope scope# handle#)))))
(defn address-of
"Gets the address of a given segment.
This value can be used as an argument to functions which take a pointer."
[addressable]
(.address ^Addressable addressable))
(defn null?
"Checks if a memory address is null."
[addr]
(.equals (MemoryAddress/NULL) addr))
(defn slice-global
"Gets a slice of the global address space.
Because this fetches from the global segment, it has no associated scope, and
therefore the reference created here cannot prevent the value from being
freed. Be careful to ensure that you are not retaining an object incorrectly."
[address size]
(.asSlice (MemorySegment/globalNativeSegment)
^MemoryAddress address (long size)))
(defn slice
"Get a slice over the `segment` with the given `offset`."
([segment offset]
(.asSlice ^MemorySegment segment (long offset)))
([segment offset size]
(.asSlice ^MemorySegment segment (long offset) (long size))))
(defn slice-into
"Get a slice into the `segment` starting at the `address`."
([address segment]
(.asSlice ^MemorySegment segment ^MemoryAddress address))
([address segment size]
(.asSlice ^MemorySegment segment ^MemoryAddress address (long size))))
(defn with-offset
"Get a new address `offset` from the old `address`."
[address offset]
(.addOffset ^MemoryAddress address (long offset)))
(defn as-segment
"Dereferences an `address` into a memory segment associated with the `scope`.
If `cleanup` is provided, it is a 0-arity function run when the scope is
closed. This can be used to register a free method for the memory, or do other
cleanup in a way that doesn't require modifying the code at the point of
freeing, and allows shared or garbage collected resources to be freed
correctly."
([address size scope]
(.asSegment ^MemoryAddress address size scope))
([address size scope cleanup]
(.asSegment ^MemoryAddress address size cleanup scope)))
(defn add-close-action!
"Adds a 0-arity function to be run when the `scope` closes."
[scope action]
(.addCloseAction ^ResourceScope scope action))
(defn copy-segment
"Copies the content to `dest` from `src`"
[dest src]
(.copyFrom ^MemorySegment dest ^MemorySegment src))
(defn clone-segment
"Clones the content of `segment` into a new segment of the same size."
([segment] (clone-segment segment (connected-scope)))
([segment scope]
(doto ^MemorySegment (alloc (.byteSize ^MemorySegment segment) scope)
(copy-segment segment))))
(defn slice-segments
"Constructs a lazy seq of `size`-length memory segments, sliced from `segment`."
[segment size]
(let [num-segments (quot (.byteSize ^MemorySegment segment) size)]
(map #(slice segment (* % size) size)
(range num-segments))))
(def primitive-types
"A set of keywords representing all the primitive types which may be passed to
or returned from native functions."
#{::byte ::short ::int ::long ::long-long
::char
::float ::double
::pointer ::void})
(defn- type-dispatch
"Gets a type dispatch value from a (potentially composite) type."
[type]
(cond
(qualified-keyword? type) type
(sequential? type) (keyword (first type))))
(defmulti primitive-type
"Gets the primitive type that is used to pass as an argument for the `type`.
This is for objects which are passed to native functions as primitive types,
but which need additional logic to be performed during serialization and
deserialization.
Returns nil for any type which does not have a primitive representation."
type-dispatch)
(defmethod primitive-type :default
[type]
(primitive-types type))
(defmethod primitive-type ::pointer
[_type]
::pointer)
(def c-prim-layout
"Map of primitive type names to the [[CLinker]] types for a method handle."
{::byte CLinker/C_CHAR
::short CLinker/C_SHORT
::int CLinker/C_INT
::long CLinker/C_LONG
::long-long CLinker/C_LONG_LONG
::char CLinker/C_CHAR
::float CLinker/C_FLOAT
::double CLinker/C_DOUBLE
::pointer CLinker/C_POINTER})
(defmulti c-layout
"Gets the layout object for a given `type`.
If a type is primitive it will return the appropriate primitive
layout (see [[c-prim-layout]]).
Otherwise, it should return a [[GroupLayout]] for the given type."
type-dispatch)
(defmethod c-layout :default
[type]
(c-prim-layout (or (primitive-type type) type)))
(def java-prim-layout
"Map of primitive type names to the Java types for a method handle."
{::byte Byte/TYPE
::short Short/TYPE
::int Integer/TYPE
::long Long/TYPE
::long-long Long/TYPE
::char Byte/TYPE
::float Float/TYPE
::double Double/TYPE
::pointer MemoryAddress
::void Void/TYPE})
(defn java-layout
"Gets the Java class to an argument of this type for a method handle.
If a type serializes to a primitive it returns return a Java primitive type.
Otherwise, it returns [[MemorySegment]]."
[type]
(java-prim-layout (or (primitive-type type) type) MemorySegment))
(defn size-of
"The size in bytes of the given `type`."
[type]
(.byteSize ^MemoryLayout (c-layout type)))
(defn alloc-instance
"Allocates a memory segment for the given `type`."
([type] (alloc-instance type (connected-scope)))
([type scope] (MemorySegment/allocateNative (long (size-of type)) ^ResourceScope scope)))
(declare serialize serialize-into)
(defmulti serialize*
"Constructs a serialized version of the `obj` and returns it.
Any new allocations made during the serialization should be tied to the given
`scope`, except in extenuating circumstances.
This method should only be implemented for types that serialize to primitives."
(fn
#_{:clj-kondo/ignore [:unused-binding]}
[obj type scope]
(type-dispatch type)))
(def ^:private primitive-cast
"Map from primitive type names to the function to cast it to a primitive."
{::byte byte
::short short
::int int
::long long
::long-long long
::char char
::float float
::double double})
(defmethod serialize* :default
[obj type _scope]
(if-let [prim (primitive-type type)]
((primitive-cast prim) obj)
(throw (ex-info "Attempted to serialize a non-primitive type with primitive methods"
{:type type
:object obj}))))
(defmethod serialize* ::pointer
[obj type scope]
(when-not (null? obj)
(if (sequential? type)
(let [segment (alloc-instance (second type) scope)]
(serialize-into obj (second type) segment scope)
(address-of segment))
obj)))
(defmulti serialize-into
"Writes a serialized version of the `obj` to the given `segment`.
Any new allocations made during the serialization should be tied to the given
`scope`, except in extenuating circumstances.
This method should be implemented for any type which does not
override [[c-layout]].
For any other type, this will serialize it as [[serialize*]] before writing
the result value into the `segment`."
(fn
#_{:clj-kondo/ignore [:unused-binding]}
[obj type segment scope]
(type-dispatch type)))
(defmethod serialize-into :default
[obj type segment scope]
(if-some [prim-layout (primitive-type type)]
(serialize-into (serialize* obj type scope) prim-layout segment scope)
(throw (ex-info "Attempted to serialize an object to a type that has not been overriden"
{:type type
:object obj}))))
(defmethod serialize-into ::byte
[obj _type segment _scope]
(MemoryAccess/setByte segment (byte obj)))
(defmethod serialize-into ::short
[obj _type segment _scope]
(MemoryAccess/setShort segment (short obj)))
(defmethod serialize-into ::int
[obj _type segment _scope]
(MemoryAccess/setInt segment (int obj)))
(defmethod serialize-into ::long
[obj _type segment _scope]
(MemoryAccess/setLong segment (long obj)))
(defmethod serialize-into ::long-long
[obj _type segment _scope]
(MemoryAccess/setLong segment (long obj)))
(defmethod serialize-into ::char
[obj _type segment _scope]
(MemoryAccess/setChar segment (char obj)))
(defmethod serialize-into ::float
[obj _type segment _scope]
(MemoryAccess/setFloat segment (float obj)))
(defmethod serialize-into ::double
[obj _type segment _scope]
(MemoryAccess/setDouble segment (double obj)))
(defmethod serialize-into ::pointer
[obj _type segment _scope]
(MemoryAccess/setAddress segment obj))
(defn serialize
"Serializes an arbitrary type.
For types which have a primitive representation, this serializes into that
representation. For types which do not, it allocates a new segment and
serializes into that."
([obj type] (serialize obj type (connected-scope)))
([obj type scope]
(if (primitive-type type)
(serialize* obj type scope)
(let [segment (alloc-instance type scope)]
(serialize-into obj type segment scope)
segment))))
(declare deserialize deserialize*)
(defmulti deserialize-from
"Deserializes the given segment into a Clojure data structure.
For types that serialize to primitives, a default implementation will
deserialize the primitive before calling [[deserialize*]]."
(fn
#_{:clj-kondo/ignore [:unused-binding]}
[segment type]
(type-dispatch type)))
(defmethod deserialize-from :default
[segment type]
(if-some [prim (primitive-type type)]
(-> segment
(deserialize-from prim)
(deserialize* type))
(throw (ex-info "Attempted to deserialize a non-primitive type that has not been overriden"
{:type type
:segment segment}))))
(defmethod deserialize-from ::byte
[segment _type]
(MemoryAccess/getByte segment))
(defmethod deserialize-from ::short
[segment _type]
(MemoryAccess/getShort segment))
(defmethod deserialize-from ::int
[segment _type]
(MemoryAccess/getInt segment))
(defmethod deserialize-from ::long
[segment _type]
(MemoryAccess/getLong segment))
(defmethod deserialize-from ::long-long
[segment _type]
(MemoryAccess/getLong segment))
(defmethod deserialize-from ::char
[segment _type]
(MemoryAccess/getChar segment))
(defmethod deserialize-from ::float
[segment _type]
(MemoryAccess/getFloat segment))
(defmethod deserialize-from ::double
[segment _type]
(MemoryAccess/getDouble segment))
(defmethod deserialize-from ::pointer
[segment type]
(cond-> (MemoryAccess/getAddress segment)
(sequential? type) (deserialize* type)))
(defmulti deserialize*
"Deserializes a primitive object into a Clojure data structure.
This is intended for use with types that are returned as a primitive but which
need additional processing before they can be returned."
(fn
#_{:clj-kondo/ignore [:unused-binding]}
[obj type]
(type-dispatch type)))
(defmethod deserialize* :default
[obj type]
(if (primitive-type type)
obj
(throw (ex-info "Attempted to deserialize a non-primitive type with primitive methods"
{:type type
:segment obj}))))
(defmethod deserialize* ::pointer
[addr type]
(when-not (null? addr)
(if (sequential? type)
(deserialize-from (slice-global addr (size-of (second type)))
(second type))
addr)))
(defn deserialize
"Deserializes an arbitrary type.
For types which have a primitive representation, this deserializes the
primitive representation. For types which do not, this deserializes out of
a segment."
[obj type]
(when-not (identical? ::void type)
((if (primitive-type type)
deserialize*
deserialize-from)
obj type)))
(defn seq-of
"Constructs a lazy sequence of `type` elements deserialized from `segment`."
[type segment]
(map #(deserialize % type) (slice-segments segment (size-of type))))
;;; C String type
(defmethod primitive-type ::c-string
[_type]
::pointer)
(defmethod serialize* ::c-string
[obj _type scope]
(if obj
(address-of (CLinker/toCString ^String obj ^ResourceScope scope))
(MemoryAddress/NULL)))
(defmethod deserialize* ::c-string
[addr _type]
(when-not (null? addr)
(CLinker/toJavaString ^MemoryAddress addr)))
;;; Union types
(defmethod c-layout ::union
[[_union types & {:as _opts} :as _type]]
(let [items (map c-layout types)]
(MemoryLayout/unionLayout
(into-array MemoryLayout items))))
(defmethod serialize-into ::union
[obj [_union _types & {:keys [dispatch extract]} :as type] segment scope]
(when-not dispatch
(throw (ex-info "Attempted to serialize a union with no dispatch function"
{:type type
:value obj})))
(let [type (dispatch obj)]
(serialize-into
(if extract
(extract type obj)
obj)
type
segment
scope)))
(defmethod deserialize-from ::union
[segment type]
(clone-segment (slice segment 0 (size-of type))))
;;; Struct types
(defmethod c-layout ::struct
[[_struct fields]]
(let [fields (for [[field-name field] fields]
(.withName ^MemoryLayout (c-layout field)
(name field-name)))]
(MemoryLayout/structLayout
(into-array MemoryLayout fields))))
(defmethod serialize-into ::struct
[obj [_struct fields] segment scope]
(loop [offset 0
fields fields]
(when (seq fields)
(let [[field type] (first fields)
size (size-of type)]
(serialize-into
(get obj field) type
(slice segment offset size) scope)
(recur (long (+ offset size)) (rest fields))))))
(defmethod deserialize-from ::struct
[segment [_struct fields]]
(loop [offset 0
fields fields
obj {}]
(if (seq fields)
(let [[field type] (first fields)
size (size-of type)]
(recur
(long (+ offset size))
(rest fields)
(assoc obj field (deserialize-from
(slice segment offset size)
type))))
obj)))
;;; Padding type
(defmethod c-layout ::padding
[[_padding size]]
(MemoryLayout/paddingLayout (* 8 size)))
(defmethod serialize-into ::padding
[_obj [_padding _size] _segment _scope]
nil)
(defmethod deserialize-from ::padding
[_segment [_padding _size]]
nil)
;;; Array types
(defmethod c-layout ::array
[[_array type count]]
(MemoryLayout/sequenceLayout
count
(c-layout type)))
(defmethod serialize-into ::array
[obj [_array type count] segment scope]
(dorun
(map #(serialize-into %1 type %2 scope)
obj
(slice-segments (slice segment 0 (* count (size-of type)))
(size-of type)))))
(defmethod deserialize-from ::array
[segment [_array type count]]
(map #(deserialize-from % type)
(slice-segments (slice segment 0 (* count (size-of type)))
(size-of type))))
;;; FFI Code loading and function access
(defn load-system-library
@ -621,20 +42,20 @@
(defn- method-type
"Gets the [[MethodType]] for a set of `args` and `ret` types."
([args] (method-type args ::void))
([args] (method-type args ::mem/void))
([args ret]
(MethodType/methodType
^Class (java-layout ret)
^"[Ljava.lang.Class;" (into-array Class (map java-layout args)))))
^Class (mem/java-layout ret)
^"[Ljava.lang.Class;" (into-array Class (map mem/java-layout args)))))
(defn- function-descriptor
"Gets the [[FunctionDescriptor]] for a set of `args` and `ret` types."
([args] (function-descriptor args ::void))
([args] (function-descriptor args ::mem/void))
([args ret]
(let [args-arr (into-array MemoryLayout (map c-layout args))]
(if-not (identical? ret ::void)
(let [args-arr (into-array MemoryLayout (map mem/c-layout args))]
(if-not (identical? ret ::mem/void)
(FunctionDescriptor/of
(c-layout ret)
(mem/c-layout ret)
args-arr)
(FunctionDescriptor/ofVoid
args-arr)))))
@ -646,26 +67,26 @@
(def ^:private load-instructions
"Mapping from primitive types to the instruction used to load them onto the stack."
{::byte :bload
::short :sload
::int :iload
::long :lload
::long-long :lload
::char :cload
::float :fload
::double :dload
::pointer :aload})
{::mem/byte :bload
::mem/short :sload
::mem/int :iload
::mem/long :lload
::mem/long-long :lload
::mem/char :cload
::mem/float :fload
::mem/double :dload
::mem/pointer :aload})
(def ^:private prim-classes
"Mapping from primitive types to their box classes."
{::byte Byte
::short Short
::int Integer
::long Long
::long-long Long
::char Character
::float Float
::double Double})
{::mem/byte Byte
::mem/short Short
::mem/int Integer
::mem/long Long
::mem/long-long Long
::mem/char Character
::mem/float Float
::mem/double Double})
(defn- to-object-asm
"Constructs a bytecode sequence to box a primitive on the top of the stack.
@ -674,10 +95,10 @@
null reference will be pushed to the stack."
[type]
(cond
(identical? ::void type) [:ldc nil]
(identical? ::pointer (primitive-type type)) []
(identical? ::mem/void type) [:ldc nil]
(identical? ::mem/pointer (mem/primitive-type type)) []
:else
(let [prim-type (some-> type primitive-type)]
(let [prim-type (some-> type mem/primitive-type)]
(if-some [prim (some-> prim-type name keyword)]
;; Box primitive
[:invokestatic (prim-classes prim-type) "valueOf" [prim (prim-classes prim-type)]]
@ -687,20 +108,20 @@
(defn- insn-layout
"Gets the type keyword or class for referring to the type in bytecode."
[type]
(if (some-> (primitive-type type) (not= ::pointer))
(if (some-> (mem/primitive-type type) (not= ::mem/pointer))
(keyword (name type))
(java-layout type)))
(mem/java-layout type)))
(def ^:private unbox-fn-for-type
"Map from type name to the name of its unboxing function."
{::byte "byteValue"
::short "shortValue"
::int "intValue"
::long "longValue"
::long-long "longValue"
::char "charValue"
::float "floatValue"
::double "doubleValue"})
{::mem/byte "byteValue"
::mem/short "shortValue"
::mem/int "intValue"
::mem/long "longValue"
::mem/long-long "longValue"
::mem/char "charValue"
::mem/float "floatValue"
::mem/double "doubleValue"})
(defn- to-prim-asm
"Constructs a bytecode sequence to unbox a primitive type on top of the stack.
@ -709,10 +130,10 @@
will be popped."
[type]
(cond
(identical? ::void type) [:pop]
(identical? ::pointer (primitive-type type)) []
(identical? ::mem/void type) [:pop]
(identical? ::mem/pointer (mem/primitive-type type)) []
:else
(let [prim-type (some-> type primitive-type)]
(let [prim-type (some-> type mem/primitive-type)]
(if-some [prim (some-> prim-type name keyword)]
[[:checkcast (prim-classes prim-type)]
[:invokevirtual (prim-classes prim-type) (unbox-fn-for-type prim-type) [prim]]]
@ -739,24 +160,24 @@
{:name :invoke
:flags #{:public}
:desc (repeat (cond-> (inc (count args))
(not (primitive-type ret)) inc)
(not (mem/primitive-type ret)) inc)
Object)
:emit [[:aload 0]
[:getfield :this "downcall_handle" MethodHandle]
(when-not (primitive-type ret)
(when-not (mem/primitive-type ret)
[[:aload 1]
[:checkcast SegmentAllocator]])
(map-indexed
(fn [idx arg]
[[:aload (cond-> (inc idx)
(not (primitive-type ret)) inc)]
(not (mem/primitive-type ret)) inc)]
(to-prim-asm arg)])
args)
[:invokevirtual MethodHandle "invokeExact"
(cond->>
(conj (mapv insn-layout args)
(insn-layout ret))
(not (primitive-type ret)) (cons SegmentAllocator))]
(not (mem/primitive-type ret)) (cons SegmentAllocator))]
(to-object-asm ret)
[:areturn]]}]})
@ -770,7 +191,7 @@
calls [[find-symbol]] on it."
[symbol-or-addr]
(if (instance? Addressable symbol-or-addr)
(address-of symbol-or-addr)
(mem/address-of symbol-or-addr)
(find-symbol symbol-or-addr)))
(defn make-downcall
@ -812,17 +233,17 @@
"Constructs a wrapper function for the `downcall` which serializes the arguments
and deserializes the return value."
[downcall arg-types ret-type]
(if (primitive-type ret-type)
(if (mem/primitive-type ret-type)
(fn native-fn [& args]
(with-open [scope (stack-scope)]
(deserialize
(apply downcall (map #(serialize %1 %2 scope) args arg-types))
(with-open [scope (mem/stack-scope)]
(mem/deserialize
(apply downcall (map #(mem/serialize %1 %2 scope) args arg-types))
ret-type)))
(fn native-fn [& args]
(with-open [scope (stack-scope)]
(deserialize
(apply downcall (scope-allocator scope)
(map #(serialize %1 %2 scope) args arg-types))
(with-open [scope (mem/stack-scope)]
(mem/deserialize
(apply downcall (mem/scope-allocator scope)
(map #(mem/serialize %1 %2 scope) args arg-types))
ret-type)))))
(defn make-serde-varargs-wrapper
@ -864,15 +285,15 @@
(def ^:private return-for-type
"Map from type name to the return instruction for that type."
{::byte :breturn
::short :sreturn
::int :ireturn
::long :lreturn
::long-long :lreturn
::char :creturn
::float :freturn
::double :dreturn
::void :return})
{::mem/byte :breturn
::mem/short :sreturn
::mem/int :ireturn
::mem/long :lreturn
::mem/long-long :lreturn
::mem/char :creturn
::mem/float :freturn
::mem/double :dreturn
::mem/void :return})
(defn- upcall-class
"Constructs a class definition for a class with a single method, `upcall`, which
@ -893,8 +314,8 @@
[:return]]}
{:name :upcall
:flags #{:public}
:desc (conj (mapv java-layout arg-types)
(java-layout ret-type))
:desc (conj (mapv mem/java-layout arg-types)
(mem/java-layout ret-type))
:emit [[:aload 0]
[:getfield :this "upcall_ifn" IFn]
(map-indexed
@ -920,21 +341,21 @@
"upcall"
(method-type arg-types ret-type)))
(defmethod primitive-type ::fn
(defmethod mem/primitive-type ::fn
[_type]
::pointer)
::mem/pointer)
(defn- upcall-serde-wrapper
"Creates a function that wraps `f` which deserializes the arguments and
serializes the return type in the [[global-scope]]."
[f arg-types ret-type]
(fn [& args]
(serialize
(apply f (map deserialize args arg-types))
(mem/serialize
(apply f (map mem/deserialize args arg-types))
ret-type
(global-scope))))
(mem/global-scope))))
(defmethod serialize* ::fn
(defmethod mem/serialize* ::fn
[f [_fn arg-types ret-type & {:keys [raw-fn?]}] scope]
(.upcallStub
(CLinker/getInstance)
@ -944,7 +365,7 @@
(function-descriptor arg-types ret-type)
scope))
(defmethod deserialize* ::fn
(defmethod mem/deserialize* ::fn
[addr [_fn arg-types ret-type & {:keys [raw-fn?]}]]
(-> addr
(downcall-handle
@ -959,7 +380,7 @@
(defn const
"Gets the value of a constant stored in `symbol-or-addr`."
[symbol-or-addr type]
(deserialize (ensure-address symbol-or-addr) [::pointer type]))
(mem/deserialize (ensure-address symbol-or-addr) [::mem/pointer type]))
(deftype StaticVariable [addr type meta]
Addressable
@ -967,7 +388,7 @@
addr)
IDeref
(deref [_]
(deserialize addr [::pointer type]))
(mem/deserialize addr [::mem/pointer type]))
IObj
(withMeta [_ meta-map]
@ -984,10 +405,10 @@
(defn freset!
"Sets the value of `static-var` to `newval`, running it through [[serialize]]."
[^StaticVariable static-var newval]
(serialize-into
(mem/serialize-into
newval (.-type static-var)
(slice-global (.-addr static-var) (size-of (.-type static-var)))
(global-scope))
(mem/slice-global (.-addr static-var) (mem/size-of (.-type static-var)))
(mem/global-scope))
newval)
(defn fswap!
@ -1070,13 +491,6 @@
:args (s/cat :libspec ::libspec)
:ret (s/map-of keyword? any?))
(s/def ::type
(s/spec
(s/nonconforming
(s/or :simple-type qualified-keyword?
:complex-type (s/cat :base-type qualified-keyword?
:type-args (s/* any?))))))
(s/def ::defcfn-args
(s/and
(s/cat :name simple-symbol?
@ -1085,7 +499,7 @@
:symbol (s/nonconforming
(s/or :string string?
:symbol simple-symbol?))
:native-arglist (s/coll-of ::type :kind vector?)
:native-arglist (s/coll-of ::mem/type :kind vector?)
:return-type qualified-keyword?
:wrapper (s/?
(s/cat
@ -1143,10 +557,10 @@
~ret-type ~(:return-type args)
~invoke (make-downcall ~(name (:symbol args)) ~args-types ~ret-type)
~(or (-> args :wrapper :native-fn) native-sym)
~(if (and (every? #(= % (primitive-type %))
~(if (and (every? #(= % (mem/primitive-type %))
(:native-arglist args))
(= (:return-type args)
(primitive-type (:return-type args))))
(mem/primitive-type (:return-type args))))
invoke
`(make-serde-wrapper ~invoke ~args-types ~ret-type))
fun# ~(if (:wrapper args)
@ -1169,35 +583,3 @@
fun#))))
(s/fdef defcfn
:args ::defcfn-args)
(defmacro defalias
"Defines a type alias from `new-type` to `aliased-type`.
This creates needed serialization and deserialization implementations for the
aliased type."
{:style/indent [:defn]}
[new-type aliased-type]
(if (primitive-type aliased-type)
`(let [aliased# ~aliased-type]
(defmethod primitive-type ~new-type
[_type#]
(primitive-type aliased#))
(defmethod serialize* ~new-type
[obj# _type# scope#]
(serialize* obj# aliased# scope#))
(defmethod deserialize* ~new-type
[obj# _type#]
(deserialize* obj# aliased#)))
`(let [aliased# ~aliased-type]
(defmethod c-layout ~new-type
[_type#]
(c-layout aliased#))
(defmethod serialize-into ~new-type
[obj# _type# segment# scope#]
(serialize-into obj# aliased# segment# scope#))
(defmethod deserialize-from ~new-type
[segment# _type#]
(deserialize-from segment# aliased#)))))
(s/fdef defalias
:args (s/cat :new-type qualified-keyword?
:aliased-type ::type))

654
src/clj/coffi/mem.clj Normal file
View file

@ -0,0 +1,654 @@
(ns coffi.mem
"Functions for managing native allocations, resource scopes, and (de)serialization.
For any new type to be implemented, three multimethods must be overriden, but
which three depends on the native representation of the type.
If the native representation of the type is a primitive (whether or not other
data beyond the primitive is associated with it, as e.g. a pointer),
then [[primitive-type]] must be overriden to return which primitive type it is
serialized as, then [[serialize*]] and [[deserialize*]] should be overriden.
If the native representation of the type is a composite type, like a union,
struct, or array, then [[c-layout]] must be overriden to return the native
layout of the type, and [[serialize-into]] and [[deserialize-from]] should be
overriden to allow marshaling values of the type into and out of memory
segments.
When writing code that manipulates a segment, it's best practice to
use [[with-acquired]] on the [[segment-scope]] in order to ensure it won't be
released during its manipulation."
(:require
[clojure.spec.alpha :as s])
(:import
(jdk.incubator.foreign
Addressable
CLinker
MemoryAccess
MemoryAddress
MemoryLayout
MemorySegment
ResourceScope
SegmentAllocator)))
;; TODO(Joshua): Ensure all the serdes acquire the scopes they use
(defn stack-scope
"Constructs a new scope for use only in this thread.
The memory allocated within this scope is cheap to allocate, like a native
stack."
^ResourceScope []
(ResourceScope/newConfinedScope))
(defn shared-scope
"Constructs a new shared scope.
This scope can be shared across threads and memory allocated in it will only
be cleaned up once every thread accessing the scope closes it."
^ResourceScope []
(ResourceScope/newSharedScope))
(defn connected-scope
"Constructs a new scope to reclaim all connected resources at once.
The scope may be shared across threads, and all resources created with it will
be cleaned up at the same time, when all references have been collected.
This type of scope cannot be closed, and therefore should not be created in
a [[with-open]] clause."
^ResourceScope []
(ResourceScope/newImplicitScope))
(defn global-scope
"Constructs the global scope, which will never reclaim its resources.
This scope may be shared across threads, but is intended mainly in cases where
memory is allocated with [[alloc]] but is either never freed or whose
management is relinquished to a native library, such as when returned from a
callback."
^ResourceScope []
(ResourceScope/globalScope))
(defn scope-allocator
"Constructs a segment allocator from the given `scope`.
This is primarily used when working with unwrapped downcall functions. When a
downcall function returns a non-primitive type, it must be provided with an
allocator."
^SegmentAllocator [^ResourceScope scope]
(SegmentAllocator/ofScope scope))
(defn segment-scope
"Gets the scope used to construct the `segment`."
^ResourceScope [segment]
(.scope ^MemorySegment segment))
(defn alloc
"Allocates `size` bytes.
If a `scope` is provided, the allocation will be reclaimed when it is closed."
([size] (alloc size (connected-scope)))
([size scope] (MemorySegment/allocateNative (long size) ^ResourceScope scope)))
(defn alloc-with
"Allocates `size` bytes using the `allocator`."
([allocator size]
(.allocate ^SegmentAllocator allocator (long size)))
([allocator size alignment]
(.allocate ^SegmentAllocator allocator (long size) (long alignment))))
(defmacro with-acquired
"Acquires a `scope` to ensure it will not be released until the `body` completes.
This is only necessary to do on shared scopes, however if you are operating on
an arbitrary passed scope, it is best practice to wrap code that interacts
with it wrapped in this."
[scope & body]
`(let [scope# ~scope
handle# (.acquire ^ResourceScope scope#)]
(try ~@body
(finally (.release ^ResourceScope scope# handle#)))))
(defn address-of
"Gets the address of a given segment.
This value can be used as an argument to functions which take a pointer."
[addressable]
(.address ^Addressable addressable))
(defn null?
"Checks if a memory address is null."
[addr]
(.equals (MemoryAddress/NULL) addr))
(defn slice-global
"Gets a slice of the global address space.
Because this fetches from the global segment, it has no associated scope, and
therefore the reference created here cannot prevent the value from being
freed. Be careful to ensure that you are not retaining an object incorrectly."
[address size]
(.asSlice (MemorySegment/globalNativeSegment)
^MemoryAddress address (long size)))
(defn slice
"Get a slice over the `segment` with the given `offset`."
([segment offset]
(.asSlice ^MemorySegment segment (long offset)))
([segment offset size]
(.asSlice ^MemorySegment segment (long offset) (long size))))
(defn slice-into
"Get a slice into the `segment` starting at the `address`."
([address segment]
(.asSlice ^MemorySegment segment ^MemoryAddress address))
([address segment size]
(.asSlice ^MemorySegment segment ^MemoryAddress address (long size))))
(defn with-offset
"Get a new address `offset` from the old `address`."
[address offset]
(.addOffset ^MemoryAddress address (long offset)))
(defn as-segment
"Dereferences an `address` into a memory segment associated with the `scope`.
If `cleanup` is provided, it is a 0-arity function run when the scope is
closed. This can be used to register a free method for the memory, or do other
cleanup in a way that doesn't require modifying the code at the point of
freeing, and allows shared or garbage collected resources to be freed
correctly."
([address size scope]
(.asSegment ^MemoryAddress address size scope))
([address size scope cleanup]
(.asSegment ^MemoryAddress address size cleanup scope)))
(defn add-close-action!
"Adds a 0-arity function to be run when the `scope` closes."
[scope action]
(.addCloseAction ^ResourceScope scope action))
(defn copy-segment
"Copies the content to `dest` from `src`"
[dest src]
(.copyFrom ^MemorySegment dest ^MemorySegment src))
(defn clone-segment
"Clones the content of `segment` into a new segment of the same size."
([segment] (clone-segment segment (connected-scope)))
([segment scope]
(doto ^MemorySegment (alloc (.byteSize ^MemorySegment segment) scope)
(copy-segment segment))))
(defn slice-segments
"Constructs a lazy seq of `size`-length memory segments, sliced from `segment`."
[segment size]
(let [num-segments (quot (.byteSize ^MemorySegment segment) size)]
(map #(slice segment (* % size) size)
(range num-segments))))
(def primitive-types
"A set of keywords representing all the primitive types which may be passed to
or returned from native functions."
#{::byte ::short ::int ::long ::long-long
::char
::float ::double
::pointer ::void})
(defn- type-dispatch
"Gets a type dispatch value from a (potentially composite) type."
[type]
(cond
(qualified-keyword? type) type
(sequential? type) (keyword (first type))))
(defmulti primitive-type
"Gets the primitive type that is used to pass as an argument for the `type`.
This is for objects which are passed to native functions as primitive types,
but which need additional logic to be performed during serialization and
deserialization.
Returns nil for any type which does not have a primitive representation."
type-dispatch)
(defmethod primitive-type :default
[type]
(primitive-types type))
(defmethod primitive-type ::pointer
[_type]
::pointer)
(def c-prim-layout
"Map of primitive type names to the [[CLinker]] types for a method handle."
{::byte CLinker/C_CHAR
::short CLinker/C_SHORT
::int CLinker/C_INT
::long CLinker/C_LONG
::long-long CLinker/C_LONG_LONG
::char CLinker/C_CHAR
::float CLinker/C_FLOAT
::double CLinker/C_DOUBLE
::pointer CLinker/C_POINTER})
(defmulti c-layout
"Gets the layout object for a given `type`.
If a type is primitive it will return the appropriate primitive
layout (see [[c-prim-layout]]).
Otherwise, it should return a [[GroupLayout]] for the given type."
type-dispatch)
(defmethod c-layout :default
[type]
(c-prim-layout (or (primitive-type type) type)))
(def java-prim-layout
"Map of primitive type names to the Java types for a method handle."
{::byte Byte/TYPE
::short Short/TYPE
::int Integer/TYPE
::long Long/TYPE
::long-long Long/TYPE
::char Byte/TYPE
::float Float/TYPE
::double Double/TYPE
::pointer MemoryAddress
::void Void/TYPE})
(defn java-layout
"Gets the Java class to an argument of this type for a method handle.
If a type serializes to a primitive it returns return a Java primitive type.
Otherwise, it returns [[MemorySegment]]."
[type]
(java-prim-layout (or (primitive-type type) type) MemorySegment))
(defn size-of
"The size in bytes of the given `type`."
[type]
(.byteSize ^MemoryLayout (c-layout type)))
(defn alloc-instance
"Allocates a memory segment for the given `type`."
([type] (alloc-instance type (connected-scope)))
([type scope] (MemorySegment/allocateNative (long (size-of type)) ^ResourceScope scope)))
(declare serialize serialize-into)
(defmulti serialize*
"Constructs a serialized version of the `obj` and returns it.
Any new allocations made during the serialization should be tied to the given
`scope`, except in extenuating circumstances.
This method should only be implemented for types that serialize to primitives."
(fn
#_{:clj-kondo/ignore [:unused-binding]}
[obj type scope]
(type-dispatch type)))
(def ^:private primitive-cast
"Map from primitive type names to the function to cast it to a primitive."
{::byte byte
::short short
::int int
::long long
::long-long long
::char char
::float float
::double double})
(defmethod serialize* :default
[obj type _scope]
(if-let [prim (primitive-type type)]
((primitive-cast prim) obj)
(throw (ex-info "Attempted to serialize a non-primitive type with primitive methods"
{:type type
:object obj}))))
(defmethod serialize* ::pointer
[obj type scope]
(when-not (null? obj)
(if (sequential? type)
(let [segment (alloc-instance (second type) scope)]
(serialize-into obj (second type) segment scope)
(address-of segment))
obj)))
(defmulti serialize-into
"Writes a serialized version of the `obj` to the given `segment`.
Any new allocations made during the serialization should be tied to the given
`scope`, except in extenuating circumstances.
This method should be implemented for any type which does not
override [[c-layout]].
For any other type, this will serialize it as [[serialize*]] before writing
the result value into the `segment`."
(fn
#_{:clj-kondo/ignore [:unused-binding]}
[obj type segment scope]
(type-dispatch type)))
(defmethod serialize-into :default
[obj type segment scope]
(if-some [prim-layout (primitive-type type)]
(serialize-into (serialize* obj type scope) prim-layout segment scope)
(throw (ex-info "Attempted to serialize an object to a type that has not been overriden"
{:type type
:object obj}))))
(defmethod serialize-into ::byte
[obj _type segment _scope]
(MemoryAccess/setByte segment (byte obj)))
(defmethod serialize-into ::short
[obj _type segment _scope]
(MemoryAccess/setShort segment (short obj)))
(defmethod serialize-into ::int
[obj _type segment _scope]
(MemoryAccess/setInt segment (int obj)))
(defmethod serialize-into ::long
[obj _type segment _scope]
(MemoryAccess/setLong segment (long obj)))
(defmethod serialize-into ::long-long
[obj _type segment _scope]
(MemoryAccess/setLong segment (long obj)))
(defmethod serialize-into ::char
[obj _type segment _scope]
(MemoryAccess/setChar segment (char obj)))
(defmethod serialize-into ::float
[obj _type segment _scope]
(MemoryAccess/setFloat segment (float obj)))
(defmethod serialize-into ::double
[obj _type segment _scope]
(MemoryAccess/setDouble segment (double obj)))
(defmethod serialize-into ::pointer
[obj _type segment _scope]
(MemoryAccess/setAddress segment obj))
(defn serialize
"Serializes an arbitrary type.
For types which have a primitive representation, this serializes into that
representation. For types which do not, it allocates a new segment and
serializes into that."
([obj type] (serialize obj type (connected-scope)))
([obj type scope]
(if (primitive-type type)
(serialize* obj type scope)
(let [segment (alloc-instance type scope)]
(serialize-into obj type segment scope)
segment))))
(declare deserialize deserialize*)
(defmulti deserialize-from
"Deserializes the given segment into a Clojure data structure.
For types that serialize to primitives, a default implementation will
deserialize the primitive before calling [[deserialize*]]."
(fn
#_{:clj-kondo/ignore [:unused-binding]}
[segment type]
(type-dispatch type)))
(defmethod deserialize-from :default
[segment type]
(if-some [prim (primitive-type type)]
(-> segment
(deserialize-from prim)
(deserialize* type))
(throw (ex-info "Attempted to deserialize a non-primitive type that has not been overriden"
{:type type
:segment segment}))))
(defmethod deserialize-from ::byte
[segment _type]
(MemoryAccess/getByte segment))
(defmethod deserialize-from ::short
[segment _type]
(MemoryAccess/getShort segment))
(defmethod deserialize-from ::int
[segment _type]
(MemoryAccess/getInt segment))
(defmethod deserialize-from ::long
[segment _type]
(MemoryAccess/getLong segment))
(defmethod deserialize-from ::long-long
[segment _type]
(MemoryAccess/getLong segment))
(defmethod deserialize-from ::char
[segment _type]
(MemoryAccess/getChar segment))
(defmethod deserialize-from ::float
[segment _type]
(MemoryAccess/getFloat segment))
(defmethod deserialize-from ::double
[segment _type]
(MemoryAccess/getDouble segment))
(defmethod deserialize-from ::pointer
[segment type]
(cond-> (MemoryAccess/getAddress segment)
(sequential? type) (deserialize* type)))
(defmulti deserialize*
"Deserializes a primitive object into a Clojure data structure.
This is intended for use with types that are returned as a primitive but which
need additional processing before they can be returned."
(fn
#_{:clj-kondo/ignore [:unused-binding]}
[obj type]
(type-dispatch type)))
(defmethod deserialize* :default
[obj type]
(if (primitive-type type)
obj
(throw (ex-info "Attempted to deserialize a non-primitive type with primitive methods"
{:type type
:segment obj}))))
(defmethod deserialize* ::pointer
[addr type]
(when-not (null? addr)
(if (sequential? type)
(deserialize-from (slice-global addr (size-of (second type)))
(second type))
addr)))
(defn deserialize
"Deserializes an arbitrary type.
For types which have a primitive representation, this deserializes the
primitive representation. For types which do not, this deserializes out of
a segment."
[obj type]
(when-not (identical? ::void type)
((if (primitive-type type)
deserialize*
deserialize-from)
obj type)))
(defn seq-of
"Constructs a lazy sequence of `type` elements deserialized from `segment`."
[type segment]
(map #(deserialize % type) (slice-segments segment (size-of type))))
;;; C String type
(defmethod primitive-type ::c-string
[_type]
::pointer)
(defmethod serialize* ::c-string
[obj _type scope]
(if obj
(address-of (CLinker/toCString ^String obj ^ResourceScope scope))
(MemoryAddress/NULL)))
(defmethod deserialize* ::c-string
[addr _type]
(when-not (null? addr)
(CLinker/toJavaString ^MemoryAddress addr)))
;;; Union types
(defmethod c-layout ::union
[[_union types & {:as _opts} :as _type]]
(let [items (map c-layout types)]
(MemoryLayout/unionLayout
(into-array MemoryLayout items))))
(defmethod serialize-into ::union
[obj [_union _types & {:keys [dispatch extract]} :as type] segment scope]
(when-not dispatch
(throw (ex-info "Attempted to serialize a union with no dispatch function"
{:type type
:value obj})))
(let [type (dispatch obj)]
(serialize-into
(if extract
(extract type obj)
obj)
type
segment
scope)))
(defmethod deserialize-from ::union
[segment type]
(clone-segment (slice segment 0 (size-of type))))
;;; Struct types
(defmethod c-layout ::struct
[[_struct fields]]
(let [fields (for [[field-name field] fields]
(.withName ^MemoryLayout (c-layout field)
(name field-name)))]
(MemoryLayout/structLayout
(into-array MemoryLayout fields))))
(defmethod serialize-into ::struct
[obj [_struct fields] segment scope]
(loop [offset 0
fields fields]
(when (seq fields)
(let [[field type] (first fields)
size (size-of type)]
(serialize-into
(get obj field) type
(slice segment offset size) scope)
(recur (long (+ offset size)) (rest fields))))))
(defmethod deserialize-from ::struct
[segment [_struct fields]]
(loop [offset 0
fields fields
obj {}]
(if (seq fields)
(let [[field type] (first fields)
size (size-of type)]
(recur
(long (+ offset size))
(rest fields)
(assoc obj field (deserialize-from
(slice segment offset size)
type))))
obj)))
;;; Padding type
(defmethod c-layout ::padding
[[_padding size]]
(MemoryLayout/paddingLayout (* 8 size)))
(defmethod serialize-into ::padding
[_obj [_padding _size] _segment _scope]
nil)
(defmethod deserialize-from ::padding
[_segment [_padding _size]]
nil)
;;; Array types
(defmethod c-layout ::array
[[_array type count]]
(MemoryLayout/sequenceLayout
count
(c-layout type)))
(defmethod serialize-into ::array
[obj [_array type count] segment scope]
(dorun
(map #(serialize-into %1 type %2 scope)
obj
(slice-segments (slice segment 0 (* count (size-of type)))
(size-of type)))))
(defmethod deserialize-from ::array
[segment [_array type count]]
(map #(deserialize-from % type)
(slice-segments (slice segment 0 (* count (size-of type)))
(size-of type))))
(s/def ::type
(s/spec
(s/nonconforming
(s/or :simple-type qualified-keyword?
:complex-type (s/cat :base-type qualified-keyword?
:type-args (s/* any?))))))
(defmacro defalias
"Defines a type alias from `new-type` to `aliased-type`.
This creates needed serialization and deserialization implementations for the
aliased type."
{:style/indent [:defn]}
[new-type aliased-type]
(if (primitive-type aliased-type)
`(let [aliased# ~aliased-type]
(defmethod primitive-type ~new-type
[_type#]
(primitive-type aliased#))
(defmethod serialize* ~new-type
[obj# _type# scope#]
(serialize* obj# aliased# scope#))
(defmethod deserialize* ~new-type
[obj# _type#]
(deserialize* obj# aliased#)))
`(let [aliased# ~aliased-type]
(defmethod c-layout ~new-type
[_type#]
(c-layout aliased#))
(defmethod serialize-into ~new-type
[obj# _type# segment# scope#]
(serialize-into obj# aliased# segment# scope#))
(defmethod deserialize-from ~new-type
[segment# _type#]
(deserialize-from segment# aliased#)))))
(s/fdef defalias
:args (s/cat :new-type qualified-keyword?
:aliased-type ::type))

View file

@ -1,31 +1,32 @@
(ns coffi.ffi-test
(:require
[clojure.test :as t]
[coffi.ffi :as sut]))
[coffi.mem :as mem]
[coffi.ffi :as ffi]))
(sut/load-library "target/ffi_test.so")
(ffi/load-library "target/ffi_test.so")
(t/deftest can-load-symbols
(t/is (not (nil? (sut/find-symbol "add_numbers")))))
(t/is (not (nil? (ffi/find-symbol "add_numbers")))))
(t/deftest can-call-primitive-fns
(t/is (= 5 ((sut/cfn "add_numbers" [::sut/int ::sut/int] ::sut/int) 2 3))))
(t/is (= 5 ((ffi/cfn "add_numbers" [::mem/int ::mem/int] ::mem/int) 2 3))))
(sut/defalias ::point
[::sut/struct
[[:x ::sut/float]
[:y ::sut/float]]])
(mem/defalias ::point
[::mem/struct
[[:x ::mem/float]
[:y ::mem/float]]])
(t/deftest can-call-with-structs
(t/is (= {:x 2.0 :y 2.0}
((sut/cfn "add_points" [::point ::point] ::point) {:x 1 :y 2} {:x 1 :y 0}))))
((ffi/cfn "add_points" [::point ::point] ::point) {:x 1 :y 2} {:x 1 :y 0}))))
(t/deftest can-call-deserialized-fn-pointers
(t/is (= "Alternate string"
(((sut/cfn "get_downcall" [::sut/int] [::sut/fn [] ::sut/c-string])
(((ffi/cfn "get_downcall" [::mem/int] [::ffi/fn [] ::mem/c-string])
1)))))
(t/deftest can-make-upcall
(t/is (= ((sut/cfn "upcall_test" [[::sut/fn [] ::sut/c-string]] ::sut/c-string)
(t/is (= ((ffi/cfn "upcall_test" [[::ffi/fn [] ::mem/c-string]] ::mem/c-string)
(fn [] "hello"))
"hello")))