diff --git a/.gitignore b/.gitignore
index 8d97753..09e23bf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
*.so*
+out/
diff --git a/README.md b/README.md
index a7ec4ec..0fca25f 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,47 @@
Dynamic library loading for Carp, based on [`dlfcn.h`](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/dlfcn.h.html).
+## Typed, ownership-aware bindings
+
+For new code, define a binder with the exact C ABI signature and use either a
+pinned or owned library:
+
+```clojure
+(load "https://github.com/carpentry-org/dynlib@0.3.0")
+
+(DynLib.defpinned-binder bind-floor (Fn [Double] Double))
+
+(defn main []
+ (match (DynLib.open-pinned "libm.so.6")
+ (Result.Error error) (IO.errorln &error)
+ (Result.Success library)
+ (match (bind-floor &library "floor")
+ (Result.Error error) (IO.errorln &error)
+ (Result.Success floor)
+ (println* &(Double.str (DynLibPinnedFn.call1 &floor 3.9))))))
+```
+
+`open-pinned` intentionally keeps the loader handle alive for the process. Its
+binder only borrows the handle, so one library can supply many functions. This
+is the conservative choice for Rust `cdylib`s, callbacks, thread-local state,
+and libraries that may retain references to their own code.
+
+`open-owned` and `defbinder` instead transfer the handle into a
+`DynLibBoundFn`. Carp closes it when the bound function is dropped, so the
+function cannot outlive its library. This scoped form binds one function per
+handle; reopening the same path normally reuses the platform loader's existing
+image and increments its reference count.
+
+Call bound functions with `DynLibPinnedFn.call0` through `call8`, or the
+corresponding `DynLibBoundFn` functions. Lookup returns each `Lambda` by value
+and does not allocate a wrapper.
+
+The binder declaration is necessarily an assertion: `dlsym` exposes neither a
+portable type nor ABI reflection. Calling a symbol through a signature that
+does not exactly match its exported C ABI is undefined behavior. Binding
+generators should therefore emit these declarations from an authoritative API
+description rather than asking users to write them manually.
+
## Usage
Provided there is a function `inc` that increments a number in a library
@@ -44,15 +85,14 @@ Do as I say, not as I do!
## Limitations
-For now, the functions that are returned by `DynLib.get` are all typed as `a`,
+The compatibility `DynLib.get` API returns functions typed as `a`,
so that we are able to encode multi-arity functions (i.e. functions with
different numbers of arguments). I’m not aware of a better way to encode this
-in the Carp type system as of yet. If there is, hit me up, because the current
-implementation breaks all type-level guarantees!
+in that API. Prefer the typed binders above, which confine the polymorphic raw
+lookup behind a generated concrete function signature.
-I’m also pretty sure that the lambdas allocated by `DynLib_dlsym` are never
-freed—because they’re returned as references—, and I’m not sure how to get
-around that!
+The compatibility lookup also allocates a `Lambda` wrapper that is not freed.
+The ownership-aware API returns the wrapper by value and avoids that leak.
diff --git a/dlfcn_helper.h b/dlfcn_helper.h
index 9799eda..b0e5925 100644
--- a/dlfcn_helper.h
+++ b/dlfcn_helper.h
@@ -14,3 +14,50 @@ Lambda* DynLib_dlsym(void* l, char* f) {
x->copy = NULL;
return x;
}
+
+/* Ownership-aware API. Unlike DynLib_dlsym, these helpers return Lambda by
+ * value and therefore do not allocate. */
+typedef void* DynLibOwned;
+typedef void* DynLibPinned;
+
+static DynLibOwned DynLib_open_owned(String* path) {
+ dlerror();
+ return dlopen(*path, RTLD_NOW | RTLD_LOCAL);
+}
+
+static DynLibPinned DynLib_open_pinned(String* path) {
+ dlerror();
+ return dlopen(*path, RTLD_NOW | RTLD_LOCAL);
+}
+
+static void DynLibOwned_delete(DynLibOwned library) {
+ if (library != NULL) (void)dlclose(library);
+}
+
+Lambda DynLib_symbol_owned(DynLibOwned* library, String* name) {
+ Lambda result = {0};
+ dlerror();
+ result.callback = dlsym(*library, *name);
+ return result;
+}
+
+Lambda DynLib_symbol_pinned(DynLibPinned* library, String* name) {
+ Lambda result = {0};
+ dlerror();
+ result.callback = dlsym(*library, *name);
+ return result;
+}
+
+static bool DynLib_valid_handle(void** library) {
+ return *library != NULL;
+}
+
+bool DynLib_valid_lambda(Lambda* function) {
+ return function->callback != NULL;
+}
+
+static String DynLib_error_string(void) {
+ const char* error = dlerror();
+ return String_from_MINUS_cstr(
+ (char*)(error == NULL ? "dynamic loader error" : error));
+}
diff --git a/dynlib.carp b/dynlib.carp
index 2bd50cf..6593070 100644
--- a/dynlib.carp
+++ b/dynlib.carp
@@ -74,3 +74,113 @@ or the `dlerror` message on failure.")
(list 'IO.errorln 'e)
'(Result.Success s)
(list set! s 's))))
+
+;; Ownership-aware typed bindings. The original Lib/get API above is retained
+;; for compatibility, but it cannot relate a function's lifetime to its loader
+;; handle and DynLib_dlsym allocates its Lambda wrapper.
+(register-type DynLibOwned)
+(register-type DynLibPinned)
+
+(defmodule DynLibOwned
+ (register delete (Fn [DynLibOwned] ()) "DynLibOwned_delete")
+ (implements delete DynLibOwned.delete))
+
+(deftype (DynLibBoundFn a) [library DynLibOwned callable a])
+(deftype (DynLibPinnedFn a) [callable a])
+
+(defmodule DynLibBoundFn
+ (private library)
+ (private callable)
+ (defn call0 [bound]
+ (let [function (DynLibBoundFn.callable bound)] (~function)))
+ (defn call1 [bound a]
+ (let [function (DynLibBoundFn.callable bound)] (~function a)))
+ (defn call2 [bound a b]
+ (let [function (DynLibBoundFn.callable bound)] (~function a b)))
+ (defn call3 [bound a b c]
+ (let [function (DynLibBoundFn.callable bound)] (~function a b c)))
+ (defn call4 [bound a b c d]
+ (let [function (DynLibBoundFn.callable bound)] (~function a b c d)))
+ (defn call5 [bound a b c d e]
+ (let [function (DynLibBoundFn.callable bound)] (~function a b c d e)))
+ (defn call6 [bound a b c d e f]
+ (let [function (DynLibBoundFn.callable bound)] (~function a b c d e f)))
+ (defn call7 [bound a b c d e f g]
+ (let [function (DynLibBoundFn.callable bound)] (~function a b c d e f g)))
+ (defn call8 [bound a b c d e f g h]
+ (let [function (DynLibBoundFn.callable bound)] (~function a b c d e f g h))))
+
+(defmodule DynLibPinnedFn
+ (private callable)
+ (defn call0 [bound]
+ (let [function (DynLibPinnedFn.callable bound)] (~function)))
+ (defn call1 [bound a]
+ (let [function (DynLibPinnedFn.callable bound)] (~function a)))
+ (defn call2 [bound a b]
+ (let [function (DynLibPinnedFn.callable bound)] (~function a b)))
+ (defn call3 [bound a b c]
+ (let [function (DynLibPinnedFn.callable bound)] (~function a b c)))
+ (defn call4 [bound a b c d]
+ (let [function (DynLibPinnedFn.callable bound)] (~function a b c d)))
+ (defn call5 [bound a b c d e]
+ (let [function (DynLibPinnedFn.callable bound)] (~function a b c d e)))
+ (defn call6 [bound a b c d e f]
+ (let [function (DynLibPinnedFn.callable bound)] (~function a b c d e f)))
+ (defn call7 [bound a b c d e f g]
+ (let [function (DynLibPinnedFn.callable bound)] (~function a b c d e f g)))
+ (defn call8 [bound a b c d e f g h]
+ (let [function (DynLibPinnedFn.callable bound)] (~function a b c d e f g h))))
+
+(defmodule DynLib
+ (register open-owned- (Fn [&String] DynLibOwned) "DynLib_open_owned")
+ (register open-pinned- (Fn [&String] DynLibPinned) "DynLib_open_pinned")
+ (register valid-owned? (Fn [&DynLibOwned] Bool) "DynLib_valid_handle")
+ (register valid-pinned? (Fn [&DynLibPinned] Bool) "DynLib_valid_handle")
+ (register symbol-owned (Fn [&DynLibOwned &String] a) "DynLib_symbol_owned")
+ (register symbol-pinned (Fn [&DynLibPinned &String] a) "DynLib_symbol_pinned")
+ (register valid-function? (Fn [&a] Bool) "DynLib_valid_lambda")
+ (register typed-error (Fn [] String) "DynLib_error_string")
+ (hidden symbol-owned)
+ (hidden symbol-pinned)
+ (hidden valid-function?)
+
+ (doc open-owned "Opens a library whose handle must remain owned by one bound
+function. Dropping that function closes the handle.")
+ (defn open-owned [path]
+ (let [library (open-owned- path)]
+ (if (valid-owned? &library)
+ (Result.Success library)
+ (Result.Error (typed-error)))))
+
+ (doc open-pinned "Opens a process-lifetime library. It is intentionally not
+unloaded, so one borrowed handle can safely produce many bindings. Prefer this
+for Rust cdylibs, callbacks, thread-local state, or foreign code that can retain
+references into the library.")
+ (defn open-pinned [path]
+ (let [library (open-pinned- path)]
+ (if (valid-pinned? &library)
+ (Result.Success library)
+ (Result.Error (typed-error)))))
+
+ (doc defbinder "Defines a typed symbol binder for an owned library. The
+declared signature is an explicit assertion about the symbol's C ABI. The
+ result owns the library handle and cannot outlive it.")
+ (defmacro defbinder [name signature]
+ `(defn %name [library symbol]
+ (let [function (the %signature
+ (DynLib.symbol-owned &library symbol))]
+ (if (DynLib.valid-function? &function)
+ (Result.Success (DynLibBoundFn.init library function))
+ (Result.Error (DynLib.typed-error))))))
+
+ (doc defpinned-binder "Defines a typed symbol binder for a pinned library.
+The declared signature is an explicit assertion about the symbol's C ABI. The
+binder only borrows the process-lifetime handle, allowing many symbols to be
+ bound from one library.")
+ (defmacro defpinned-binder [name signature]
+ `(defn %name [library symbol]
+ (let [function (the %signature
+ (DynLib.symbol-pinned library symbol))]
+ (if (DynLib.valid-function? &function)
+ (Result.Success (DynLibPinnedFn.init function))
+ (Result.Error (DynLib.typed-error)))))))
diff --git a/test/typed.carp b/test/typed.carp
new file mode 100644
index 0000000..b6b346f
--- /dev/null
+++ b/test/typed.carp
@@ -0,0 +1,36 @@
+(load "../dynlib.carp")
+
+(DynLib.defbinder bind-owned-floor (Fn [Double] Double))
+(DynLib.defpinned-binder bind-pinned-floor (Fn [Double] Double))
+(DynLib.defpinned-binder bind-pinned-ceil (Fn [Double] Double))
+(DynLib.defpinned-binder bind-pinned-pow (Fn [Double Double] Double))
+(DynLib.defpinned-binder bind-missing (Fn [] ()))
+
+(defn main []
+ (do
+ (match (DynLib.open-owned "libm.so.6")
+ (Result.Error error) (IO.errorln &error)
+ (Result.Success library)
+ (match (bind-owned-floor library "floor")
+ (Result.Error error) (IO.errorln &error)
+ (Result.Success function)
+ (assert (= 3.0 (DynLibBoundFn.call1 &function 3.9)))))
+ (match (DynLib.open-pinned "libm.so.6")
+ (Result.Error error) (IO.errorln &error)
+ (Result.Success library)
+ (do
+ (match (bind-pinned-floor &library "floor")
+ (Result.Error error) (IO.errorln &error)
+ (Result.Success function)
+ (assert (= 4.0 (DynLibPinnedFn.call1 &function 4.9))))
+ (match (bind-pinned-ceil &library "ceil")
+ (Result.Error error) (IO.errorln &error)
+ (Result.Success function)
+ (assert (= 6.0 (DynLibPinnedFn.call1 &function 5.1))))
+ (match (bind-pinned-pow &library "pow")
+ (Result.Error error) (IO.errorln &error)
+ (Result.Success function)
+ (assert (= 8.0 (DynLibPinnedFn.call2 &function 2.0 3.0))))
+ (match (bind-missing &library "dynlib_missing_symbol")
+ (Result.Error _) ()
+ (Result.Success _) (assert false))))))