From 00946348189db8135deb399e04d83c11196bce67 Mon Sep 17 00:00:00 2001 From: Joshua Suskalo Date: Thu, 21 Sep 2023 10:38:31 -0500 Subject: [PATCH 01/20] Update target version to 21 --- build.clj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.clj b/build.clj index bdbf2ba..90919c4 100644 --- a/build.clj +++ b/build.clj @@ -52,8 +52,8 @@ (b/process {:command-args ["javac" "--enable-preview" "src/java/coffi/ffi/Loader.java" "-d" class-dir - "-target" "19" - "-source" "19"]}) + "-target" "21" + "-source" "21"]}) opts) (defn- write-pom From 36f1685718f78f3977bb4291316f1b3fb9f95faf Mon Sep 17 00:00:00 2001 From: Joshua Suskalo Date: Thu, 21 Sep 2023 10:39:15 -0500 Subject: [PATCH 02/20] Update to `.find(String)` api in loader --- src/java/coffi/ffi/Loader.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/java/coffi/ffi/Loader.java b/src/java/coffi/ffi/Loader.java index 8830118..ef458fc 100644 --- a/src/java/coffi/ffi/Loader.java +++ b/src/java/coffi/ffi/Loader.java @@ -10,6 +10,8 @@ import java.lang.foreign.*; */ public class Loader { + static SymbolLookup lookup = Linker.nativeLinker().defaultLookup().or(SymbolLookup.loaderLookup()); + /** * Loads a library from a given absolute file path. * @@ -37,7 +39,6 @@ public class Loader { * @param symbol The name of the symbol to load from a library. */ public static MemorySegment findSymbol(String symbol) { - return Linker.nativeLinker().defaultLookup().lookup(symbol) - .orElseGet(() -> SymbolLookup.loaderLookup().lookup(symbol).orElse(null)); + return lookup.find(symbol).orElse(null); } } From 2325abf53b9e88d35144cc3421ce17d6ad928911 Mon Sep 17 00:00:00 2001 From: Joshua Suskalo Date: Wed, 27 Dec 2023 09:14:44 -0600 Subject: [PATCH 03/20] WIP Update `coffi.mem` to JDK 21 --- src/clj/coffi/mem.clj | 175 +++++++++++++++++++++++++----------------- 1 file changed, 103 insertions(+), 72 deletions(-) diff --git a/src/clj/coffi/mem.clj b/src/clj/coffi/mem.clj index abafd54..ca12422 100644 --- a/src/clj/coffi/mem.clj +++ b/src/clj/coffi/mem.clj @@ -23,11 +23,11 @@ [clojure.spec.alpha :as s]) (:import (java.lang.foreign - Addressable - MemoryAddress + AddressLayout + Arena MemoryLayout MemorySegment - MemorySession + MemorySegment$Scope SegmentAllocator ValueLayout ValueLayout$OfByte @@ -36,50 +36,79 @@ ValueLayout$OfLong ValueLayout$OfChar ValueLayout$OfFloat - ValueLayout$OfDouble - ValueLayout$OfAddress) + ValueLayout$OfDouble) (java.lang.ref Cleaner) (java.nio ByteOrder))) (set! *warn-on-reflection* true) -(defn stack-session +(defn confined-arena + "Constructs a new arena for use only in this thread. + + The memory allocated within this arena is cheap to allocate, like a native + stack. + + The memory allocated within this arena will be cleared once it is closed, so + it is usually a good idea to create it in a [[with-open]] clause." + (^Arena [] + (Arena/ofConfined))) + +(defn ^:deprecated stack-session "Constructs a new session for use only in this thread. The memory allocated within this session is cheap to allocate, like a native stack." - (^MemorySession [] - (MemorySession/openConfined)) - (^MemorySession [^Cleaner cleaner] - (MemorySession/openConfined cleaner))) + (^Arena [] + (confined-arena)) + (^Arena [^Cleaner cleaner] + (assert false "Stack sessions with associated cleaners have been removed."))) (defn ^:deprecated 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." - ^MemorySession [] - (stack-session)) + ^Arena [] + (confined-arena)) -(defn shared-session +(defn shared-arena + "Constructs a new shared memory arena. + + This arena can be shared across threads and memory allocated in it will only + be cleaned up once any thread accessing the arena closes it." + (^Arena [] + (Arena/ofShared))) + +(defn ^:deprecated shared-session "Constructs a new shared memory session. This session can be shared across threads and memory allocated in it will only be cleaned up once every thread accessing the session closes it." - (^MemorySession [] - (MemorySession/openShared)) - (^MemorySession [^Cleaner cleaner] - (MemorySession/openShared cleaner))) + (^Arena [] + (shared-arena)) + (^Arena [^Cleaner cleaner] + (assert false "Shared sessions with associated cleaners have been removed."))) (defn ^:deprecated 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." - ^MemorySession [] - (shared-session)) + ^Arena [] + (shared-arena)) -(defn connected-session +(defn auto-arena + "Constructs a new memory arena that is managed by the garbage collector. + + The arena 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 arena cannot be closed, and therefore should not be created in + a [[with-open]] clause." + ^Arena [] + (Arena/ofAuto)) + +(defn ^:deprecated connected-session "Constructs a new memory session to reclaim all connected resources at once. The session may be shared across threads, and all resources created with it @@ -87,8 +116,8 @@ This type of session cannot be closed, and therefore should not be created in a [[with-open]] clause." - ^MemorySession [] - (MemorySession/openImplicit)) + ^Arena [] + (auto-arena)) (defn ^:deprecated connected-scope "Constructs a new scope to reclaim all connected resources at once. @@ -98,18 +127,28 @@ This type of scope cannot be closed, and therefore should not be created in a [[with-open]] clause." - ^MemorySession [] - (connected-session)) + ^Arena [] + (auto-arena)) -(defn global-session +(defn global-arena + "Constructs the global arena, which will never reclaim its resources. + + This arena 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." + ^Arena [] + (Arena/global)) + +(defn ^:deprecated global-session "Constructs the global session, which will never reclaim its resources. This session 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." - ^MemorySession [] - (MemorySession/global)) + ^Arena [] + (global-arena)) (defn ^:deprecated global-scope "Constructs the global scope, which will never reclaim its resources. @@ -118,17 +157,17 @@ 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." - ^MemorySession [] - (global-session)) + ^Arena [] + (global-arena)) -(defn session-allocator +(defn ^:deprecated session-allocator "Constructs a segment allocator from the given `session`. 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 [^MemorySession session] - (SegmentAllocator/newNativeArena session)) + ^SegmentAllocator [^Arena session] + (assert false "Segment allocators can no longer be constructed from sessions.")) (defn ^:deprecated scope-allocator "Constructs a segment allocator from the given `scope`. @@ -136,26 +175,26 @@ 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 [^MemorySession scope] - (session-allocator scope)) + ^SegmentAllocator [^Arena scope] + (assert false "Segment allocators can no longer be constructed from scopes.")) -(defn segment-session +(defn ^:deprecated segment-session "Gets the memory session used to construct the `segment`." - ^MemorySession [segment] - (.session ^MemorySegment segment)) + ^Arena [^MemorySegment segment] + (assert false "Segment sessions no longer provide linkes to the arenas that allocated them.")) -(defn ^:deprecated segment-scope - "Gets the scope used to construct the `segment`." - ^MemorySession [segment] - (segment-session segment)) +(defn segment-scope + "Gets the scope associated with the `segment`." + ^MemorySegment$Scope [segment] + (.scope ^MemorySegment segment)) (defn alloc "Allocates `size` bytes. - If a `session` is provided, the allocation will be reclaimed when it is closed." - (^MemorySegment [size] (alloc size (connected-session))) - (^MemorySegment [size session] (MemorySegment/allocateNative (long size) ^MemorySession session)) - (^MemorySegment [size alignment session] (MemorySegment/allocateNative (long size) (long alignment) ^MemorySession session))) + If an `arena` is provided, the allocation will be reclaimed when it is closed." + (^MemorySegment [size] (alloc size (auto-arena))) + (^MemorySegment [size arena] (.allocate ^Arena arena (long size))) + (^MemorySegment [size alignment arena] (.allocate ^Arena arena (long size) (long alignment)))) (defn alloc-with "Allocates `size` bytes using the `allocator`." @@ -164,7 +203,7 @@ (^MemorySegment [allocator size alignment] (.allocate ^SegmentAllocator allocator (long size) (long alignment)))) -(defmacro with-acquired +(defmacro ^:deprecated with-acquired "Acquires one or more `sessions` until the `body` completes. This is only necessary to do on shared sessions, however if you are operating @@ -172,17 +211,7 @@ interacts with it wrapped in this." {:style/indent 1} [sessions & body] - (if (seq sessions) - `(let [session# ~(first sessions) - res# (volatile! ::invalid-value)] - (.whileAlive - ^MemorySession session# - (^:once fn* [] - (vreset! res# - (with-acquired [~@(rest sessions)] - ~@body)))) - @res#) - `(do ~@body))) + (assert false "Support was removed for keeping a shared arena open.")) (s/fdef with-acquired :args (s/cat :sessions any? :body (s/* any?))) @@ -191,20 +220,20 @@ "Gets the address of a given segment. This value can be used as an argument to functions which take a pointer." - ^MemoryAddress [addressable] - (.address ^Addressable addressable)) + ^long [addressable] + (.address ^MemorySegment addressable)) (defn null? "Checks if a memory address is null." [addr] - (or (.equals (MemoryAddress/NULL) addr) (not addr))) + (or (.equals (MemorySegment/NULL) addr) (not addr))) (defn address? "Checks if an object is a memory address. `nil` is considered an address." [addr] - (or (nil? addr) (instance? MemoryAddress addr))) + (or (nil? addr) (instance? MemorySegment addr))) (defn slice "Get a slice over the `segment` with the given `offset`." @@ -213,24 +242,28 @@ (^MemorySegment [segment offset size] (.asSlice ^MemorySegment segment (long offset) (long size)))) -(defn slice-into +(defn ^:deprecated slice-into "Get a slice into the `segment` starting at the `address`." (^MemorySegment [address segment] - (.asSlice ^MemorySegment segment ^MemoryAddress address)) + (.asSlice ^MemorySegment segment (address-of address))) (^MemorySegment [address segment size] - (.asSlice ^MemorySegment segment ^MemoryAddress address (long size)))) + (.asSlice ^MemorySegment segment (address-of address) (long size)))) -(defn with-offset +(defn ^:deprecated with-offset "Get a new address `offset` from the old `address`." - ^MemoryAddress [address offset] - (.addOffset ^MemoryAddress address (long offset))) + ^MemorySegment [address offset] + (slice address offset)) +;; TODO(Joshua): Figure out if this can be replicated with [[Cleaner]] +#_ (defn add-close-action! "Adds a 0-arity function to be run when the `session` closes." [^MemorySession session ^Runnable action] (.addCloseAction session action) nil) +;; TODO(Joshua): Determine if this needs to exist at all +#_ (defn as-segment "Dereferences an `address` into a memory segment associated with the `session`." (^MemorySegment [^MemoryAddress address size] @@ -243,9 +276,7 @@ Returns `dest`." ^MemorySegment [^MemorySegment dest ^MemorySegment src] - (with-acquired [(segment-session src) (segment-session dest)] - (.copyFrom dest src) - dest)) + (.copyFrom dest src)) (defn clone-segment "Clones the content of `segment` into a new segment of the same size." @@ -721,12 +752,12 @@ ([segment value] `(let [segment# ~segment value# ~value] - (.set ^MemorySegment segment# ^ValueLayout$OfAddress pointer-layout 0 ^Addressable value#))) + (.set ^MemorySegment segment# ^ValueLayout$OfAddress pointer-layout 0 ^MemorySegment value#))) ([segment offset value] `(let [segment# ~segment offset# ~offset value# ~value] - (.set ^MemorySegment segment# ^ValueLayout$OfAddress pointer-layout offset# ^Addressable value#))))} + (.set ^MemorySegment segment# ^ValueLayout$OfAddress pointer-layout offset# ^MemorySegment value#))))} (^MemoryAddress [^MemorySegment segment ^MemoryAddress value] (.set segment ^ValueLayout$OfAddress pointer-layout 0 value)) (^MemoryAddress [^MemorySegment segment ^long offset ^MemoryAddress value] From b7092b4af6c7c2331c3f46dd42fe14f1f4cf9c45 Mon Sep 17 00:00:00 2001 From: Joshua Suskalo Date: Mon, 29 Jan 2024 06:22:39 -0600 Subject: [PATCH 04/20] WIP Finish updating files to fully be on JDK 21 This is still incomplete, I get crashes on upcalls. --- src/clj/coffi/ffi.clj | 50 ++++++++--------- src/clj/coffi/layout.clj | 2 +- src/clj/coffi/mem.clj | 114 +++++++++++++++++++-------------------- 3 files changed, 78 insertions(+), 88 deletions(-) diff --git a/src/clj/coffi/ffi.clj b/src/clj/coffi/ffi.clj index 371aeef..bff4e99 100644 --- a/src/clj/coffi/ffi.clj +++ b/src/clj/coffi/ffi.clj @@ -14,10 +14,9 @@ MethodHandles MethodType) (java.lang.foreign - Addressable Linker + Linker$Option FunctionDescriptor - MemoryAddress MemoryLayout MemorySegment SegmentAllocator))) @@ -56,7 +55,8 @@ (defn- downcall-handle "Gets the [[MethodHandle]] for the function at the `sym`." [sym function-descriptor] - (.downcallHandle (Linker/nativeLinker) sym function-descriptor)) + (.downcallHandle (Linker/nativeLinker) sym function-descriptor + (make-array Linker$Option 0))) (def ^:private load-instructions "Mapping from primitive types to the instruction used to load them onto the stack." @@ -130,15 +130,6 @@ [:invokevirtual (prim-classes prim-type) (unbox-fn-for-type prim-type) [prim]]] [])))) -(defn- coerce-addressable - "If the passed `type` is [[MemoryAddress]], returns [[Addressable]], otherwise returns `type`. - - This is used to declare the return types of upcall stubs." - [type] - (if (= type MemoryAddress) - Addressable - type)) - (defn- downcall-class "Class definition for an implementation of [[IFn]] which calls a closed over method handle without reflection, unboxing primitives when needed." @@ -175,7 +166,7 @@ args) [:invokevirtual MethodHandle "invokeExact" (cond->> - (conj (mapv (comp coerce-addressable insn-layout) args) + (conj (mapv insn-layout args) (insn-layout ret)) (not (mem/primitive-type ret)) (cons SegmentAllocator))] (to-object-asm ret) @@ -343,7 +334,7 @@ ;; taking restargs, and so the downcall must be applied (-> `(~@(when (symbol? args) [`apply]) ~downcall-sym - ~@(when allocator? [`(mem/session-allocator ~session)]) + ~@(when allocator? [`(mem/arena-allocator ~session)]) ~@(if (symbol? args) [args] args)) @@ -410,7 +401,7 @@ (fn native-fn [& args] (with-open [session (mem/stack-session)] (mem/deserialize-from - (apply downcall (mem/session-allocator session) + (apply downcall (mem/arena-allocator session) (map #(mem/serialize %1 %2 session) args arg-types)) ret-type))))) @@ -435,6 +426,7 @@ If your `args` and `ret` are constants, then it is more efficient to call [[make-downcall]] followed by [[make-serde-wrapper]] because the latter has an inline definition which will result in less overhead from serdes." + ;; TODO(Joshua): Add an inline arity for when the args and ret types are constant [symbol args ret] (-> symbol (make-downcall args ret) @@ -489,7 +481,7 @@ {:name :upcall :flags #{:public} :desc (conj (mapv insn-layout arg-types) - (coerce-addressable (insn-layout ret-type))) + (insn-layout ret-type)) :emit [[:aload 0] [:getfield :this "upcall_ifn" IFn] (loop [types arg-types @@ -505,7 +497,7 @@ inc))) acc)) [:invokeinterface IFn "invoke" (repeat (inc (count arg-types)) Object)] - (to-prim-asm (coerce-addressable ret-type)) + (to-prim-asm ret-type) [(return-for-type ret-type :areturn)]]}]}) (defn- upcall @@ -518,7 +510,7 @@ ([args] (method-type args ::mem/void)) ([args ret] (MethodType/methodType - ^Class (coerce-addressable (mem/java-layout ret)) + ^Class (mem/java-layout ret) ^"[Ljava.lang.Class;" (into-array Class (map mem/java-layout args))))) (defn- upcall-handle @@ -545,21 +537,22 @@ (mem/global-session)))) (defmethod mem/serialize* ::fn - [f [_fn arg-types ret-type & {:keys [raw-fn?]}] session] + [f [_fn arg-types ret-type & {:keys [raw-fn?]}] arena] + (println "Attempting to serialize function of type" (str ret-type "(*)(" (clojure.string/join "," arg-types) ")")) (.upcallStub (Linker/nativeLinker) - (cond-> f - (not raw-fn?) (upcall-serde-wrapper arg-types ret-type) - :always (upcall-handle arg-types ret-type)) - (function-descriptor arg-types ret-type) - session)) + ^MethodHandle (cond-> f + (not raw-fn?) (upcall-serde-wrapper arg-types ret-type) + :always (upcall-handle arg-types ret-type)) + ^FunctionDescriptor (function-descriptor arg-types ret-type) + ^Arena arena + (make-array Linker$Option 0))) (defmethod mem/deserialize* ::fn [addr [_fn arg-types ret-type & {:keys [raw-fn?]}]] (when-not (mem/null? addr) (vary-meta - (-> addr - (MemorySegment/ofAddress mem/pointer-size (mem/connected-session)) + (-> ^MemorySegment addr (downcall-handle (function-descriptor arg-types ret-type)) (downcall-fn arg-types ret-type) (cond-> (not raw-fn?) (make-serde-wrapper arg-types ret-type))) @@ -640,9 +633,8 @@ See [[freset!]], [[fswap!]]." [symbol-or-addr type] - (StaticVariable. (mem/as-segment (.address (ensure-symbol symbol-or-addr)) - (mem/size-of type) - (mem/global-session)) + (StaticVariable. (.reinterpret ^MemorySegment (ensure-symbol symbol-or-addr) + ^long (mem/size-of type)) type (atom nil))) (defmacro defvar diff --git a/src/clj/coffi/layout.clj b/src/clj/coffi/layout.clj index 15a2db6..810a41f 100644 --- a/src/clj/coffi/layout.clj +++ b/src/clj/coffi/layout.clj @@ -24,7 +24,7 @@ (pos? r) (conj [::padding [::mem/padding (- align r)]]) :always (conj field)) fields)) - (let [strongest-alignment (mem/align-of struct-spec) + (let [strongest-alignment (reduce max (map (comp mem/align-of second) (nth struct-spec 1))) r (rem offset strongest-alignment)] (cond-> aligned-fields (pos? r) (conj [::padding [::mem/padding (- strongest-alignment r)]])))))] diff --git a/src/clj/coffi/mem.clj b/src/clj/coffi/mem.clj index ca12422..fab0981 100644 --- a/src/clj/coffi/mem.clj +++ b/src/clj/coffi/mem.clj @@ -13,11 +13,7 @@ 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-session]] in order to ensure it won't be - released during its manipulation." + segments." (:require [clojure.set :as set] [clojure.spec.alpha :as s]) @@ -160,6 +156,17 @@ ^Arena [] (global-arena)) +(defn arena-allocator + "Constructs a [[SegmentAllocator]] from the given [[Arena]]. + + 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 [^Arena scope] + (reify SegmentAllocator + (^MemorySegment allocate [_this ^long byte-size ^long byte-alignment] + (.allocate scope ^long byte-size ^long byte-alignment)))) + (defn ^:deprecated session-allocator "Constructs a segment allocator from the given `session`. @@ -167,7 +174,7 @@ downcall function returns a non-primitive type, it must be provided with an allocator." ^SegmentAllocator [^Arena session] - (assert false "Segment allocators can no longer be constructed from sessions.")) + (arena-allocator session)) (defn ^:deprecated scope-allocator "Constructs a segment allocator from the given `scope`. @@ -176,7 +183,7 @@ downcall function returns a non-primitive type, it must be provided with an allocator." ^SegmentAllocator [^Arena scope] - (assert false "Segment allocators can no longer be constructed from scopes.")) + (arena-allocator scope)) (defn ^:deprecated segment-session "Gets the memory session used to construct the `segment`." @@ -282,8 +289,7 @@ "Clones the content of `segment` into a new segment of the same size." (^MemorySegment [segment] (clone-segment segment (connected-session))) (^MemorySegment [^MemorySegment segment session] - (with-acquired [(segment-session segment) session] - (copy-segment ^MemorySegment (alloc (.byteSize segment) session) segment)))) + (copy-segment ^MemorySegment (alloc (.byteSize segment) session) segment))) (defn slice-segments "Constructs a lazy seq of `size`-length memory segments, sliced from `segment`." @@ -338,7 +344,7 @@ "The [[MemoryLayout]] for a c-sized double in [[native-endian]] [[ByteOrder]]." ValueLayout/JAVA_DOUBLE) -(def ^ValueLayout$OfAddress pointer-layout +(def ^AddressLayout pointer-layout "The [[MemoryLayout]] for a native pointer in [[native-endian]] [[ByteOrder]]." ValueLayout/ADDRESS) @@ -548,20 +554,20 @@ (.get segment (.withOrder ^ValueLayout$OfDouble double-layout byte-order) offset))) (defn read-address - "Reads a [[MemoryAddress]] from the `segment`, at an optional `offset`." + "Reads an address from the `segment`, at an optional `offset`, wrapped in a [[MemorySegment]]." {:inline (fn read-address-inline ([segment] `(let [segment# ~segment] - (.get ^MemorySegment segment# ^ValueLayout$OfAddress pointer-layout 0))) + (.get ^MemorySegment segment# ^AddressLayout pointer-layout 0))) ([segment offset] `(let [segment# ~segment offset# ~offset] - (.get ^MemorySegment segment# ^ValueLayout$OfAddress pointer-layout offset#))))} - (^MemoryAddress [^MemorySegment segment] - (.get segment ^ValueLayout$OfAddress pointer-layout 0)) - (^MemoryAddress [^MemorySegment segment ^long offset] - (.get segment ^ValueLayout$OfAddress pointer-layout offset))) + (.get ^MemorySegment segment# ^AddressLayout pointer-layout offset#))))} + (^MemorySegment [^MemorySegment segment] + (.get segment ^AddressLayout pointer-layout 0)) + (^MemorySegment [^MemorySegment segment ^long offset] + (.get segment ^AddressLayout pointer-layout offset))) (defn write-byte "Writes a [[byte]] to the `segment`, at an optional `offset`." @@ -746,22 +752,22 @@ (.set segment (.withOrder ^ValueLayout$OfDouble double-layout byte-order) offset value))) (defn write-address - "Writes a [[MemoryAddress]] to the `segment`, at an optional `offset`." + "Writes the address of the [[MemorySegment]] `value` to the `segment`, at an optional `offset`." {:inline (fn write-address-inline ([segment value] `(let [segment# ~segment value# ~value] - (.set ^MemorySegment segment# ^ValueLayout$OfAddress pointer-layout 0 ^MemorySegment value#))) + (.set ^MemorySegment segment# ^AddressLayout pointer-layout 0 ^MemorySegment value#))) ([segment offset value] `(let [segment# ~segment offset# ~offset value# ~value] - (.set ^MemorySegment segment# ^ValueLayout$OfAddress pointer-layout offset# ^MemorySegment value#))))} - (^MemoryAddress [^MemorySegment segment ^MemoryAddress value] - (.set segment ^ValueLayout$OfAddress pointer-layout 0 value)) - (^MemoryAddress [^MemorySegment segment ^long offset ^MemoryAddress value] - (.set segment ^ValueLayout$OfAddress pointer-layout offset value))) + (.set ^MemorySegment segment# ^AddressLayout pointer-layout offset# ^MemorySegment value#))))} + ([^MemorySegment segment ^MemorySegment value] + (.set segment ^AddressLayout pointer-layout 0 value)) + ([^MemorySegment segment ^long offset ^MemorySegment value] + (.set segment ^AddressLayout pointer-layout offset value))) (defn- type-dispatch "Gets a type dispatch value from a (potentially composite) type." @@ -898,7 +904,7 @@ ::char Byte/TYPE ::float Float/TYPE ::double Double/TYPE - ::pointer MemoryAddress + ::pointer MemorySegment ::void Void/TYPE}) (defn java-layout @@ -925,8 +931,8 @@ (defn alloc-instance "Allocates a memory segment for the given `type`." - (^MemorySegment [type] (alloc-instance type (connected-session))) - (^MemorySegment [type session] (MemorySegment/allocateNative ^long (size-of type) ^MemorySession session))) + (^MemorySegment [type] (alloc-instance type (auto-arena))) + (^MemorySegment [type arena] (.allocate ^Arena arena ^long (size-of type) ^long (align-of type)))) (declare serialize serialize-into) @@ -980,12 +986,11 @@ [obj type session] (if-not (null? obj) (if (sequential? type) - (with-acquired [session] - (let [segment (alloc-instance (second type) session)] - (serialize-into obj (second type) segment session) - (address-of segment))) + (let [segment (alloc-instance (second type) session)] + (serialize-into obj (second type) segment session) + (address-of segment)) obj) - (MemoryAddress/NULL))) + (MemorySegment/NULL))) (defmethod serialize* ::void [_obj _type _session] @@ -1001,10 +1006,7 @@ override [[c-layout]]. For any other type, this will serialize it as [[serialize*]] before writing - the result value into the `segment`. - - Implementations of this should be inside a [[with-acquired]] block for the - `session` if they perform multiple memory operations." + the result value into the `segment`." (fn #_{:clj-kondo/ignore [:unused-binding]} [obj type segment session] @@ -1013,8 +1015,7 @@ (defmethod serialize-into :default [obj type segment session] (if-some [prim-layout (primitive-type type)] - (with-acquired [(segment-session segment) session] - (serialize-into (serialize* obj type session) prim-layout segment session)) + (serialize-into (serialize* obj type session) prim-layout segment session) (throw (ex-info "Attempted to serialize an object to a type that has not been overridden" {:type type :object obj})))) @@ -1059,11 +1060,10 @@ (defmethod serialize-into ::pointer [obj type segment session] - (with-acquired [(segment-session segment) session] - (write-address - segment - (cond-> obj - (sequential? type) (serialize* type session))))) + (write-address + segment + (cond-> obj + (sequential? type) (serialize* type session)))) (defn serialize "Serializes an arbitrary type. @@ -1085,10 +1085,7 @@ "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*]]. - - Implementations of this should be inside a [[with-acquired]] block for the the - `segment`'s session if they perform multiple memory operations." + deserialize the primitive before calling [[deserialize*]]." (fn #_{:clj-kondo/ignore [:unused-binding]} [segment type] @@ -1144,9 +1141,8 @@ (defmethod deserialize-from ::pointer [segment type] - (with-acquired [(segment-session segment)] - (cond-> (read-address segment) - (sequential? type) (deserialize* type)))) + (cond-> (read-address segment) + (sequential? type) (deserialize* type))) (defmulti deserialize* "Deserializes a primitive object into a Clojure data structure. @@ -1196,8 +1192,11 @@ [addr type] (when-not (null? addr) (if (sequential? type) - (deserialize-from (as-segment addr (size-of (second type))) - (second type)) + (let [target-type (second type)] + (deserialize-from + (.reinterpret ^MemorySegment (read-address addr) + ^long (size-of target-type)) + target-type)) addr))) (defmethod deserialize* ::void @@ -1219,8 +1218,7 @@ (defn seq-of "Constructs a lazy sequence of `type` elements deserialized from `segment`." [type segment] - (with-acquired [(segment-session segment)] - (map #(deserialize % type) (slice-segments segment (size-of type))))) + (map #(deserialize % type) (slice-segments segment (size-of type)))) ;;; Raw composite types ;; TODO(Joshua): Ensure that all the raw values don't have anything happen on @@ -1251,13 +1249,13 @@ (defmethod serialize* ::c-string [obj _type session] (if obj - (address-of (.allocateUtf8String (session-allocator session) ^String obj)) - (MemoryAddress/NULL))) + (address-of (.allocateUtf8String (arena-allocator session) ^String obj)) + (MemorySegment/NULL))) (defmethod deserialize* ::c-string [addr _type] (when-not (null? addr) - (.getUtf8String ^MemoryAddress addr 0))) + (.getUtf8String (.reinterpret ^MemorySegment addr Integer/MAX_VALUE) 0))) ;;; Union types @@ -1328,7 +1326,7 @@ (defmethod c-layout ::padding [[_padding size]] - (MemoryLayout/paddingLayout (* 8 size))) + (MemoryLayout/paddingLayout size)) (defmethod serialize-into ::padding [_obj [_padding _size] _segment _session] From c43bce4768fd752a83b3ab63e83ac211beada0b0 Mon Sep 17 00:00:00 2001 From: Kristin Rutenkolk Date: Mon, 10 Jun 2024 16:33:30 -0700 Subject: [PATCH 05/20] switch to java22 support --- build.clj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.clj b/build.clj index 90919c4..7d00c55 100644 --- a/build.clj +++ b/build.clj @@ -52,8 +52,8 @@ (b/process {:command-args ["javac" "--enable-preview" "src/java/coffi/ffi/Loader.java" "-d" class-dir - "-target" "21" - "-source" "21"]}) + "-target" "22" + "-source" "22"]}) opts) (defn- write-pom From a04fe7253f1b2bd9857b12ff2a23b74addfae157 Mon Sep 17 00:00:00 2001 From: Kristin Rutenkolk Date: Wed, 12 Jun 2024 11:10:52 -0700 Subject: [PATCH 06/20] change String handling to match new FFI API --- src/clj/coffi/mem.clj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/clj/coffi/mem.clj b/src/clj/coffi/mem.clj index fab0981..d9e620d 100644 --- a/src/clj/coffi/mem.clj +++ b/src/clj/coffi/mem.clj @@ -1247,15 +1247,15 @@ ::pointer) (defmethod serialize* ::c-string - [obj _type session] + [obj _type ^Arena session] (if obj - (address-of (.allocateUtf8String (arena-allocator session) ^String obj)) + (address-of (.allocateFrom session ^String obj)) (MemorySegment/NULL))) (defmethod deserialize* ::c-string [addr _type] (when-not (null? addr) - (.getUtf8String (.reinterpret ^MemorySegment addr Integer/MAX_VALUE) 0))) + (.getString (.reinterpret ^MemorySegment addr Integer/MAX_VALUE) 0))) ;;; Union types From c740745d49d0c0fba670c5b1d7bc12bfaa73f80c Mon Sep 17 00:00:00 2001 From: Kristin Rutenkolk Date: Wed, 12 Jun 2024 15:52:44 -0700 Subject: [PATCH 07/20] restructure build.clj functions to not need to accept options and return them, so that they may return useful data relevant to their task --- build.clj | 56 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/build.clj b/build.clj index 7d00c55..af7aee3 100644 --- a/build.clj +++ b/build.clj @@ -36,9 +36,8 @@ (defn clean "Deletes the `target/` directory." - [opts] - (b/delete {:path target-dir}) - opts) + [] + (b/delete {:path target-dir})) (defn- exists? "Checks if a file composed of the given path segments exists." @@ -47,18 +46,17 @@ (defn compile-java "Compiles java classes required for interop." - [opts] + [] (.mkdirs (io/file class-dir)) (b/process {:command-args ["javac" "--enable-preview" "src/java/coffi/ffi/Loader.java" "-d" class-dir "-target" "22" - "-source" "22"]}) - opts) + "-source" "22"]})) (defn- write-pom "Writes a pom file if one does not already exist." - [opts] + [] (when-not (exists? (b/pom-path {:lib lib-coord :class-dir class-dir})) (b/write-pom {:basis basis @@ -72,44 +70,40 @@ :src-dirs source-dirs}) (b/copy-file {:src (b/pom-path {:lib lib-coord :class-dir class-dir}) - :target (str target-dir "pom.xml")})) - opts) + :target (str target-dir "pom.xml")}))) (defn pom "Generates a `pom.xml` file in the `target/classes/META-INF` directory. If `:pom/output-path` is specified, copies the resulting pom file to it." [opts] - (write-pom opts) + (write-pom) (when-some [path (:output-path opts)] (b/copy-file {:src (b/pom-path {:lib lib-coord :class-dir class-dir}) - :target path})) - opts) + :target path}))) (defn- copy-resources "Copies the resources from the [[resource-dirs]] to the [[class-dir]]." - [opts] + [] (b/copy-dir {:target-dir class-dir - :src-dirs resource-dirs}) - opts) + :src-dirs resource-dirs})) (defn jar "Generates a `coffi.jar` file in the `target/` directory. This is a thin jar including only the sources." [opts] (write-pom opts) - (compile-java opts) - (copy-resources opts) + (compile-java) + (copy-resources) (when-not (exists? target-dir jar-file) (b/copy-dir {:target-dir class-dir :src-dirs source-dirs}) (b/jar {:class-dir class-dir - :jar-file jar-file})) - opts) + :jar-file jar-file}))) (defn compile-test-library "Compiles the C test code for running the tests." - [opts] + [] (let [c-files (->> c-test-dirs (map io/file) (mapcat file-seq) @@ -118,8 +112,20 @@ (.mkdirs (io/file target-dir)) (b/process {:command-args (concat ["clang" "-fpic" "-shared"] c-files - ["-o" test-c-library])})) - opts) + ["-o" test-c-library])}))) + +(defn- arities [fn-var] (:arglists (meta fn-var))) +(defn- niladic-only? [fn-var] + (let [ari (arities fn-var) + one-arity? (= 1 (count ari)) + niladic? (= 0 (count (first ari)))] + (and one-arity? niladic?))) +(defn- call-optionally [fn-sym arg] + (let [fn-var (resolve fn-sym)] + (if (niladic-only? fn-var) + (fn-var) + (fn-var arg)))) +(defn- call-optionally-with [arg] #(call-optionally % arg)) (defn run-tasks "Runs a series of tasks with a set of options. @@ -127,8 +133,4 @@ the option keys are passed unmodified." [opts] (binding [*ns* (find-ns 'build)] - (reduce - (fn [opts task] - ((resolve task) opts)) - opts - (:tasks opts)))) + (run! (call-optionally-with opts) (:tasks opts)))) From 4a7659cf2a345aecebc39df82872050b30403bfb Mon Sep 17 00:00:00 2001 From: Kristin Rutenkolk Date: Mon, 17 Jun 2024 10:57:15 -0700 Subject: [PATCH 08/20] add more upcall tests --- build.clj | 16 ++++++++++++++++ test/c/ffi_test.c | 8 ++++++++ test/clj/coffi/ffi_test.clj | 10 ++++++++++ 3 files changed, 34 insertions(+) diff --git a/build.clj b/build.clj index af7aee3..756cc2d 100644 --- a/build.clj +++ b/build.clj @@ -134,3 +134,19 @@ [opts] (binding [*ns* (find-ns 'build)] (run! (call-optionally-with opts) (:tasks opts)))) + + +(def prep-all ['compile-java 'compile-test-library]) + +(comment + + (compile-java) + + (compile-test-library) + + (run-tasks prep-all) + + (compile-test-library) + +) + diff --git a/test/c/ffi_test.c b/test/c/ffi_test.c index c689e87..d0d4c03 100644 --- a/test/c/ffi_test.c +++ b/test/c/ffi_test.c @@ -26,10 +26,18 @@ CString upcall_test(StringFactory fun) { return fun(); } +int upcall_test2(int (*f)(void)) { + return f(); +} + int counter = 0; static char* responses[] = { "Hello, world!", "Goodbye friend.", "co'oi prenu" }; +char* upcall_test_int_fn_string_ret(int (*f)(void)) { + return responses[f()]; +} + CString get_string1(void) { return responses[counter++ % 3]; } diff --git a/test/clj/coffi/ffi_test.clj b/test/clj/coffi/ffi_test.clj index 15cc4b6..45a724a 100644 --- a/test/clj/coffi/ffi_test.clj +++ b/test/clj/coffi/ffi_test.clj @@ -32,6 +32,16 @@ (fn [] "hello")) "hello"))) +(t/deftest can-make-upcall2 + (t/is (= ((ffi/cfn "upcall_test2" [[::ffi/fn [] ::mem/int]] ::mem/int) + (fn [] 6)) + 5))) + +(t/deftest can-make-upcall-int-fn-string-ret + (t/is (= ((ffi/cfn "upcall_test_int_fn_string_ret" [[::ffi/fn [] ::mem/int]] ::mem/c-string) + (fn [] 2)) + "co'oi prenu"))) + (mem/defalias ::alignment-test (layout/with-c-layout [::mem/struct From 85d52f64b7980839602a5de7de81eeb54a087761 Mon Sep 17 00:00:00 2001 From: Kristin Rutenkolk Date: Wed, 26 Jun 2024 13:22:47 +0200 Subject: [PATCH 09/20] fix upcalls with strings --- deps.edn | 2 +- src/clj/coffi/ffi.clj | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/deps.edn b/deps.edn index d0e1469..a6c2599 100644 --- a/deps.edn +++ b/deps.edn @@ -1,6 +1,6 @@ {:paths ["src/clj" "target/classes" "resources"] :deps {org.clojure/clojure {:mvn/version "1.11.1"} - insn/insn {:mvn/version "0.2.1"}} + insn/insn {:mvn/version "0.5.4"}} :deps/prep-lib {:alias :build :fn build/compile-java diff --git a/src/clj/coffi/ffi.clj b/src/clj/coffi/ffi.clj index bff4e99..730908d 100644 --- a/src/clj/coffi/ffi.clj +++ b/src/clj/coffi/ffi.clj @@ -135,6 +135,7 @@ method handle without reflection, unboxing primitives when needed." [args ret] {:flags #{:public :final} + :version 8 :super clojure.lang.AFunction :fields [{:name "downcall_handle" :type MethodHandle @@ -466,6 +467,7 @@ boxes any primitives passed to it and calls a closed over [[IFn]]." [arg-types ret-type] {:flags #{:public :final} + :version 8 :fields [{:name "upcall_ifn" :type IFn :flags #{:final}}] @@ -497,6 +499,10 @@ inc))) acc)) [:invokeinterface IFn "invoke" (repeat (inc (count arg-types)) Object)] + (if (identical? ::mem/pointer (mem/primitive-type ret-type)) + [[:checkcast Long] + [:invokevirtual Long "longValue" [:long]] + [:invokestatic MemorySegment "ofAddress" [:long MemorySegment] true]]) (to-prim-asm ret-type) [(return-for-type ret-type :areturn)]]}]}) @@ -538,7 +544,6 @@ (defmethod mem/serialize* ::fn [f [_fn arg-types ret-type & {:keys [raw-fn?]}] arena] - (println "Attempting to serialize function of type" (str ret-type "(*)(" (clojure.string/join "," arg-types) ")")) (.upcallStub (Linker/nativeLinker) ^MethodHandle (cond-> f From ef1dcfb6d077daa396beeada3824ee5e7d73911d Mon Sep 17 00:00:00 2001 From: Kristin Rutenkolk Date: Wed, 26 Jun 2024 15:13:41 +0200 Subject: [PATCH 10/20] fix ill-defined test --- test/clj/coffi/ffi_test.clj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/clj/coffi/ffi_test.clj b/test/clj/coffi/ffi_test.clj index 45a724a..ac075e4 100644 --- a/test/clj/coffi/ffi_test.clj +++ b/test/clj/coffi/ffi_test.clj @@ -29,12 +29,12 @@ (t/deftest can-make-upcall (t/is (= ((ffi/cfn "upcall_test" [[::ffi/fn [] ::mem/c-string]] ::mem/c-string) - (fn [] "hello")) - "hello"))) + (fn [] "hello from clojure from c from clojure")) + "hello from clojure from c from clojure"))) (t/deftest can-make-upcall2 (t/is (= ((ffi/cfn "upcall_test2" [[::ffi/fn [] ::mem/int]] ::mem/int) - (fn [] 6)) + (fn [] 5)) 5))) (t/deftest can-make-upcall-int-fn-string-ret From 6fc010191492e5622dc7fcbea490235c45b68995 Mon Sep 17 00:00:00 2001 From: Kristin Rutenkolk Date: Thu, 27 Jun 2024 14:40:28 +0200 Subject: [PATCH 11/20] fix build/jar to call write-pom without arguments --- build.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.clj b/build.clj index 756cc2d..a909708 100644 --- a/build.clj +++ b/build.clj @@ -92,7 +92,7 @@ "Generates a `coffi.jar` file in the `target/` directory. This is a thin jar including only the sources." [opts] - (write-pom opts) + (write-pom) (compile-java) (copy-resources) (when-not (exists? target-dir jar-file) From 854914350cd36feb33a222ae4e9f96c82cfe9df0 Mon Sep 17 00:00:00 2001 From: Kristin Rutenkolk Date: Thu, 27 Jun 2024 15:17:14 +0200 Subject: [PATCH 12/20] rename reference to nonexistent MemoryAddress class to MemorySegment --- src/clj/coffi/ffi.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clj/coffi/ffi.clj b/src/clj/coffi/ffi.clj index 730908d..3d35d01 100644 --- a/src/clj/coffi/ffi.clj +++ b/src/clj/coffi/ffi.clj @@ -292,7 +292,7 @@ ;; cast null pointers to something understood by panama (#{::mem/pointer} type) - `(or ~sym (MemoryAddress/NULL)) + `(or ~sym (MemorySegment/NULL)) (mem/primitive-type type) `(mem/serialize* ~sym ~type-sym ~session) From 56481ea9e3eba2369f83a7d0f43385a433e78822 Mon Sep 17 00:00:00 2001 From: Kristin Rutenkolk Date: Thu, 27 Jun 2024 15:40:42 +0200 Subject: [PATCH 13/20] revert regression as it breaks user code (for now) --- src/clj/coffi/mem.clj | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/clj/coffi/mem.clj b/src/clj/coffi/mem.clj index d9e620d..71fbd42 100644 --- a/src/clj/coffi/mem.clj +++ b/src/clj/coffi/mem.clj @@ -269,8 +269,6 @@ (.addCloseAction session action) nil) -;; TODO(Joshua): Determine if this needs to exist at all -#_ (defn as-segment "Dereferences an `address` into a memory segment associated with the `session`." (^MemorySegment [^MemoryAddress address size] From b68f8af5494219ef438d61f61f118e35d6265ded Mon Sep 17 00:00:00 2001 From: Kristin Rutenkolk Date: Thu, 27 Jun 2024 16:07:38 +0200 Subject: [PATCH 14/20] revert build.clj changes --- build.clj | 74 +++++++++++++++++++++---------------------------------- 1 file changed, 28 insertions(+), 46 deletions(-) diff --git a/build.clj b/build.clj index a909708..1830d09 100644 --- a/build.clj +++ b/build.clj @@ -36,8 +36,9 @@ (defn clean "Deletes the `target/` directory." - [] - (b/delete {:path target-dir})) + [opts] + (b/delete {:path target-dir}) + opts) (defn- exists? "Checks if a file composed of the given path segments exists." @@ -46,17 +47,18 @@ (defn compile-java "Compiles java classes required for interop." - [] + [opts] (.mkdirs (io/file class-dir)) (b/process {:command-args ["javac" "--enable-preview" "src/java/coffi/ffi/Loader.java" "-d" class-dir "-target" "22" - "-source" "22"]})) + "-source" "22"]}) + opts) (defn- write-pom "Writes a pom file if one does not already exist." - [] + [opts] (when-not (exists? (b/pom-path {:lib lib-coord :class-dir class-dir})) (b/write-pom {:basis basis @@ -70,40 +72,44 @@ :src-dirs source-dirs}) (b/copy-file {:src (b/pom-path {:lib lib-coord :class-dir class-dir}) - :target (str target-dir "pom.xml")}))) + :target (str target-dir "pom.xml")}) + opts)) (defn pom "Generates a `pom.xml` file in the `target/classes/META-INF` directory. If `:pom/output-path` is specified, copies the resulting pom file to it." [opts] - (write-pom) + (write-pom opts) (when-some [path (:output-path opts)] (b/copy-file {:src (b/pom-path {:lib lib-coord :class-dir class-dir}) - :target path}))) + :target path})) + opts) (defn- copy-resources "Copies the resources from the [[resource-dirs]] to the [[class-dir]]." - [] + [opts] (b/copy-dir {:target-dir class-dir - :src-dirs resource-dirs})) + :src-dirs resource-dirs}) + opts) (defn jar "Generates a `coffi.jar` file in the `target/` directory. This is a thin jar including only the sources." [opts] - (write-pom) - (compile-java) - (copy-resources) + (write-pom opts) + (compile-java opts) + (copy-resources opts) (when-not (exists? target-dir jar-file) (b/copy-dir {:target-dir class-dir :src-dirs source-dirs}) (b/jar {:class-dir class-dir - :jar-file jar-file}))) + :jar-file jar-file})) + opts) (defn compile-test-library "Compiles the C test code for running the tests." - [] + [opts] (let [c-files (->> c-test-dirs (map io/file) (mapcat file-seq) @@ -112,20 +118,8 @@ (.mkdirs (io/file target-dir)) (b/process {:command-args (concat ["clang" "-fpic" "-shared"] c-files - ["-o" test-c-library])}))) - -(defn- arities [fn-var] (:arglists (meta fn-var))) -(defn- niladic-only? [fn-var] - (let [ari (arities fn-var) - one-arity? (= 1 (count ari)) - niladic? (= 0 (count (first ari)))] - (and one-arity? niladic?))) -(defn- call-optionally [fn-sym arg] - (let [fn-var (resolve fn-sym)] - (if (niladic-only? fn-var) - (fn-var) - (fn-var arg)))) -(defn- call-optionally-with [arg] #(call-optionally % arg)) + ["-o" test-c-library])}) + opts)) (defn run-tasks "Runs a series of tasks with a set of options. @@ -133,20 +127,8 @@ the option keys are passed unmodified." [opts] (binding [*ns* (find-ns 'build)] - (run! (call-optionally-with opts) (:tasks opts)))) - - -(def prep-all ['compile-java 'compile-test-library]) - -(comment - - (compile-java) - - (compile-test-library) - - (run-tasks prep-all) - - (compile-test-library) - -) - + (reduce + (fn [opts task] + ((resolve task) opts)) + opts + (:tasks opts)))) From 854d6ce850d15691d9b6f05986cbac3087e7adc3 Mon Sep 17 00:00:00 2001 From: Kristin Rutenkolk Date: Thu, 27 Jun 2024 16:26:22 +0200 Subject: [PATCH 15/20] migrate as-segment to jdk22 API --- src/clj/coffi/mem.clj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/clj/coffi/mem.clj b/src/clj/coffi/mem.clj index 71fbd42..972b12d 100644 --- a/src/clj/coffi/mem.clj +++ b/src/clj/coffi/mem.clj @@ -271,10 +271,10 @@ (defn as-segment "Dereferences an `address` into a memory segment associated with the `session`." - (^MemorySegment [^MemoryAddress address size] - (MemorySegment/ofAddress address (long size) (connected-session))) - (^MemorySegment [^MemoryAddress address size session] - (MemorySegment/ofAddress address (long size) session))) + (^MemorySegment [^MemorySegment address size] + (.reinterpret (MemorySegment/ofAddress address) (long size) (connected-session) nil)) + (^MemorySegment [^MemorySegment address size session] + (.reinterpret (MemorySegment/ofAddress address) (long size) session nil))) (defn copy-segment "Copies the content to `dest` from `src`. From b37b975b17bf00f3fc02a021ec794d6f1a95b3fa Mon Sep 17 00:00:00 2001 From: Kristin Rutenkolk Date: Fri, 28 Jun 2024 11:41:24 +0200 Subject: [PATCH 16/20] revert build.clj changes fully --- build.clj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build.clj b/build.clj index 1830d09..7d00c55 100644 --- a/build.clj +++ b/build.clj @@ -72,8 +72,8 @@ :src-dirs source-dirs}) (b/copy-file {:src (b/pom-path {:lib lib-coord :class-dir class-dir}) - :target (str target-dir "pom.xml")}) - opts)) + :target (str target-dir "pom.xml")})) + opts) (defn pom "Generates a `pom.xml` file in the `target/classes/META-INF` directory. @@ -118,8 +118,8 @@ (.mkdirs (io/file target-dir)) (b/process {:command-args (concat ["clang" "-fpic" "-shared"] c-files - ["-o" test-c-library])}) - opts)) + ["-o" test-c-library])})) + opts) (defn run-tasks "Runs a series of tasks with a set of options. From 510763f68eed485c1884ea3946038f7c5121db20 Mon Sep 17 00:00:00 2001 From: Kristin Rutenkolk Date: Tue, 23 Jul 2024 15:04:44 +0200 Subject: [PATCH 17/20] add new failing test case --- test/c/ffi_test.c | 42 +++++++++++++++++++++---------------- test/clj/coffi/ffi_test.clj | 9 ++++++++ 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/test/c/ffi_test.c b/test/c/ffi_test.c index d0d4c03..8c98f48 100644 --- a/test/c/ffi_test.c +++ b/test/c/ffi_test.c @@ -39,35 +39,41 @@ char* upcall_test_int_fn_string_ret(int (*f)(void)) { } CString get_string1(void) { - return responses[counter++ % 3]; + return responses[counter++ % 3]; } CString get_string2(void) { - return "Alternate string"; + return "Alternate string"; } StringFactory get_downcall(int whichString) { - switch (whichString % 2) { - case 0: - return get_string1; - case 1: - return get_string2; - default: - return 0; - } + switch (whichString % 2) { + case 0: + return get_string1; + case 1: + return get_string2; + default: + return 0; + } } typedef struct alignment_test { - char a; - double x; - float y; + char a; + double x; + float y; } AlignmentTest; AlignmentTest get_struct() { - AlignmentTest ret = {}; - ret.a = 'x'; - ret.x = 3.14; - ret.y = 42.0; + AlignmentTest ret = {}; + ret.a = 'x'; + ret.x = 3.14; + ret.y = 42.0; - return ret; + return ret; } + +void test_call_with_trailing_string_arg(int a, int b, char* text) { + printf("call of `test_call_with_trailing_string_arg` with a=%i b=%i text='%s'",a,b,text); + return; +} + diff --git a/test/clj/coffi/ffi_test.clj b/test/clj/coffi/ffi_test.clj index ac075e4..45c1930 100644 --- a/test/clj/coffi/ffi_test.clj +++ b/test/clj/coffi/ffi_test.clj @@ -59,3 +59,12 @@ (ffi/freset! (ffi/static-variable "counter" ::mem/int) 1) (t/is (= ((ffi/cfn "get_string1" [] ::mem/c-string)) "Goodbye friend."))) + +(t/deftest can-call-with-trailing-string-arg + (t/is + (= + ((ffi/cfn "test_call_with_trailing_string_arg" + [::mem/int ::mem/int ::mem/c-string] + ::mem/void) + 1 2 "third arg") + ))) From 0bf267e44ad0460f246caae7fdc6589fa4d4d1f4 Mon Sep 17 00:00:00 2001 From: Kristin Rutenkolk Date: Tue, 23 Jul 2024 15:35:21 +0200 Subject: [PATCH 18/20] add more failing test cases --- test/c/ffi_test.c | 3 ++- test/clj/coffi/ffi_test.clj | 4 ++-- test/clj/coffi/mem_test.clj | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 test/clj/coffi/mem_test.clj diff --git a/test/c/ffi_test.c b/test/c/ffi_test.c index 8c98f48..e38ff30 100644 --- a/test/c/ffi_test.c +++ b/test/c/ffi_test.c @@ -73,7 +73,8 @@ AlignmentTest get_struct() { } void test_call_with_trailing_string_arg(int a, int b, char* text) { - printf("call of `test_call_with_trailing_string_arg` with a=%i b=%i text='%s'",a,b,text); + printf("call of `test_call_with_trailing_string_arg` with a=%i b=%i text='%s'",1,2,text); + printf("\r "); return; } diff --git a/test/clj/coffi/ffi_test.clj b/test/clj/coffi/ffi_test.clj index 45c1930..d45e3ae 100644 --- a/test/clj/coffi/ffi_test.clj +++ b/test/clj/coffi/ffi_test.clj @@ -66,5 +66,5 @@ ((ffi/cfn "test_call_with_trailing_string_arg" [::mem/int ::mem/int ::mem/c-string] ::mem/void) - 1 2 "third arg") - ))) + 1 2 "third arg")))) + diff --git a/test/clj/coffi/mem_test.clj b/test/clj/coffi/mem_test.clj new file mode 100644 index 0000000..b52c1eb --- /dev/null +++ b/test/clj/coffi/mem_test.clj @@ -0,0 +1,32 @@ +(ns coffi.mem-test + (:require + [clojure.test :as t] + [coffi.ffi :as ffi] + [coffi.layout :as layout] + [coffi.mem :as mem]) + (:import + (java.lang.foreign + AddressLayout + Arena + MemoryLayout + MemorySegment + MemorySegment$Scope + SegmentAllocator + ValueLayout + ValueLayout$OfByte + ValueLayout$OfShort + ValueLayout$OfInt + ValueLayout$OfLong + ValueLayout$OfChar + ValueLayout$OfFloat + ValueLayout$OfDouble) + (java.lang.ref Cleaner) + (java.nio ByteOrder))) + +(ffi/load-library "target/ffi_test.so") + +(t/deftest can-serialize-string + (t/is + (instance? MemorySegment (mem/serialize "this is a string" ::mem/c-string)))) + + From 4160b1cb6861225c6b7e0d7ec0f45837a793d9ce Mon Sep 17 00:00:00 2001 From: Kristin Rutenkolk Date: Tue, 23 Jul 2024 15:39:58 +0200 Subject: [PATCH 19/20] consistently serialize strings to MemorySegment instead of addresses (Longs) and remove ad-hoc upcall-class Long to MemorySegment conversion --- src/clj/coffi/ffi.clj | 4 ---- src/clj/coffi/mem.clj | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/clj/coffi/ffi.clj b/src/clj/coffi/ffi.clj index 3d35d01..365fa10 100644 --- a/src/clj/coffi/ffi.clj +++ b/src/clj/coffi/ffi.clj @@ -499,10 +499,6 @@ inc))) acc)) [:invokeinterface IFn "invoke" (repeat (inc (count arg-types)) Object)] - (if (identical? ::mem/pointer (mem/primitive-type ret-type)) - [[:checkcast Long] - [:invokevirtual Long "longValue" [:long]] - [:invokestatic MemorySegment "ofAddress" [:long MemorySegment] true]]) (to-prim-asm ret-type) [(return-for-type ret-type :areturn)]]}]}) diff --git a/src/clj/coffi/mem.clj b/src/clj/coffi/mem.clj index 972b12d..23139c1 100644 --- a/src/clj/coffi/mem.clj +++ b/src/clj/coffi/mem.clj @@ -1247,7 +1247,7 @@ (defmethod serialize* ::c-string [obj _type ^Arena session] (if obj - (address-of (.allocateFrom session ^String obj)) + (.allocateFrom session ^String obj) (MemorySegment/NULL))) (defmethod deserialize* ::c-string From 917141b6a2cea4f4c556f5a54c51c5688c5b5590 Mon Sep 17 00:00:00 2001 From: Joshua Suskalo Date: Wed, 24 Jul 2024 18:27:17 -0500 Subject: [PATCH 20/20] Use 4-space indentation in the C code --- test/c/ffi_test.c | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/test/c/ffi_test.c b/test/c/ffi_test.c index e38ff30..24e6959 100644 --- a/test/c/ffi_test.c +++ b/test/c/ffi_test.c @@ -39,42 +39,42 @@ char* upcall_test_int_fn_string_ret(int (*f)(void)) { } CString get_string1(void) { - return responses[counter++ % 3]; + return responses[counter++ % 3]; } CString get_string2(void) { - return "Alternate string"; + return "Alternate string"; } StringFactory get_downcall(int whichString) { - switch (whichString % 2) { - case 0: - return get_string1; - case 1: - return get_string2; - default: - return 0; - } + switch (whichString % 2) { + case 0: + return get_string1; + case 1: + return get_string2; + default: + return 0; + } } typedef struct alignment_test { - char a; - double x; - float y; + char a; + double x; + float y; } AlignmentTest; AlignmentTest get_struct() { - AlignmentTest ret = {}; - ret.a = 'x'; - ret.x = 3.14; - ret.y = 42.0; + AlignmentTest ret = {}; + ret.a = 'x'; + ret.x = 3.14; + ret.y = 42.0; - return ret; + return ret; } void test_call_with_trailing_string_arg(int a, int b, char* text) { - printf("call of `test_call_with_trailing_string_arg` with a=%i b=%i text='%s'",1,2,text); - printf("\r "); - return; + printf("call of `test_call_with_trailing_string_arg` with a=%i b=%i text='%s'",1,2,text); + printf("\r "); + return; }