Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
*.so*
out/
52 changes: 46 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

<hr/>

Expand Down
47 changes: 47 additions & 0 deletions dlfcn_helper.h
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
110 changes: 110 additions & 0 deletions dynlib.carp
Original file line number Diff line number Diff line change
Expand Up @@ -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)))))))
36 changes: 36 additions & 0 deletions test/typed.carp
Original file line number Diff line number Diff line change
@@ -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))))))
Loading