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))))))