Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Load a library at runtime

A program can choose a native library while it runs. Plenty still compiles the function signatures ahead of time. Start with a small library, saved as counter.plenty:

enum CalcError:
    Overflow

class Counter:
    value: i64
    def __del__(self) -> ():
        drop(print("released"))

export def create(value: i64) -> Result[Counter, AllocError] = "counter_create":
    Ok(Counter(value))

export def read(counter: &Counter) -> i64 = "counter_read":
    counter.value

export def increment(counter: &mut Counter) -> Result[(), CalcError] = "counter_increment":
    result = checked:
        counter.value + 1
    counter.value = result.map_error(def(error: OverflowError[i64]) -> CalcError: CalcError.Overflow)?
    Ok(())

Build the shared library and its runtime-loading module:

plenty --shared-library counter.plenty --library-name counter -o libcounter.so
plenty --runtime-interface counter.plenty --library-name counter -o plugin.plentyi

Now save this consumer beside plugin.plentyi and run it from the directory containing libcounter.so. No library link arguments are needed.

import plugin

def main() -> Result[(), AllocError | IoError | LoadError | plugin.CalcError]:
    path = "./libcounter.so"
    library = plugin.load(&path)?
    mut counter = library.create(42)?
    library.increment(&mut counter)?
    print(library.read(&counter))?
    mut maximum = library.create(i64.MAX)?
    match library.increment(&mut maximum):
        case Err(plugin.CalcError.Overflow):
            print("overflow", library.read(&maximum) == i64.MAX)?
        case Ok(_):
            print("unexpected success")?
    drop(maximum)
    drop(library)
    drop(counter)

    missing = "./missing-library.so"
    match plugin.load(&missing):
        case Ok(_):
            print("unexpected library")?
        case Err(error):
            print(error)?
    Ok(())
43
overflow True
released
released
LoadError.OpenFailed

Loading checks the library’s exact interface contract and every required export. A changed contract reports LoadError.IncompatibleContract; a missing export reports LoadError.MissingSymbol. Imports alone load nothing.

The loaded methods preserve the exports’ signatures, borrows, and errors. Dropping library does not invalidate its objects: counter still uses the matching destructor, and native code stays loaded until process exit.

Keep objects with the library instance that created them. Passing an object to a different loaded instance stops with a diagnostic before making the native call. Loading trusts native code: metadata checks compatibility, and library constructors may run before a later check reports an error.

The lifecycle trace uses drop(print(...)) deliberately: cleanup cannot return an error, and a failed diagnostic write must not interrupt cleanup. For output that must succeed, provide an explicit fallible operation and propagate its result before leaving the scope.

The maximum-value object exercises the loaded error-status path and is released separately. The fallible increment maps arithmetic overflow to the payload-free CalcError.Overflow at the ABI boundary. It leaves the object unchanged on failure; callers must handle its status before continuing.