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.