Dynamic Modules
Zynx supports typed runtime loading of shared libraries for plugin and extension use cases.
Overview
Dynamic modules are loaded with require("...").
- Type information is resolved at compile time from embedded
zynx.meta.v1metadata. - Runtime module and symbol loading is lazy and cached.
- Load failures and symbol-resolution failures use normal Zynx
try/catchflow. - The recommended surface is the named-handle form.
const math = try require("plugins/libmath.so");
let sum = try math.add(10, 10);
require is a reserved builtin. It cannot be shadowed by a local binding or function.
Compile-Time Contract
require is intentionally strict:
- The argument must be a string literal.
- The literal must end in a shared-library suffix:
.so,.dylib, or.dll. - The shared library must be available at compile time so the compiler can read its metadata.
- The library must contain embedded
zynx.meta.v1metadata. - The embedded metadata must pass schema, target, and ABI validation.
Zynx no longer falls back to sibling .zxh files for require(); the metadata must be embedded into the shared library itself.
Note: .dll suffixes are accepted by the compile-time contract, but runtime .dll loading is not implemented yet.
Basic Use
The documented v1 contract is a named handle:
const math = try require("plugins/libmath.so");
let x = try math.add(1, 2);
let answer = try math.ANSWER;
Chained access is also supported:
let x = require("plugins/libmath.so").add(1, 2) catch 0;
What You Can Export
Dynamic modules currently support:
- Exported functions
- Exported globals, read lazily and read-only
Dynamic modules do not currently support consuming exported types as dynamic values or type names. For example, these are rejected:
- exported
struct,enum,interface, ortypedeclarations in type position - struct literal construction through a dynamic handle
- generic callable exports
If you want to expose a type-backed API dynamically, export concrete wrapper functions instead.
Error Handling
require() itself is fallible and should be unwrapped with try or catch:
fn use_plugin() -> i32! {
const math = try require("plugins/libmath.so");
return math.add(40, 2);
}
For local recovery:
const math = require("plugins/libmath.so") catch null;
On require() itself, catch (err) binds err: str:
const msg: str = require("plugins/libmath.so") catch (err) err;
Member Access and Types
Dynamic function calls and global reads are also runtime lookups, so they can be fallible even when the exported ABI type is plain.
- If the exported function or global has plain type
T, the dynamic expression behaves likeT!. - If the exported function already returns
U!, that error-union type is preserved. - Missing symbols propagate through normal error handling instead of trapping.
Examples:
const math = try require("plugins/libmath.so");
let a = math.add(1, 2) catch 0;
let b = math.ANSWER catch 0;
Global writes are not allowed:
const math = try require("plugins/libmath.so");
math.ANSWER = 7; // error
Runtime Behavior
- Handle creation is lazy on first use.
- Symbol lookup is lazy and cached per module and symbol.
- Repeated
require()calls for the same canonical library path share one loader cache entry. - Loader failures surface through error-union handling with stable error messages.
Search Paths and Loader Settings
require() resolves literal paths in this order:
- Relative to the source file containing the
require(...) - The process working directory
zynx.native.dynamic.searchPathsfromzynx.json- CLI
--lib-pathdirectories
Project configuration:
{
"zynx": {
"native": {
"dynamic": {
"searchPaths": ["plugins", "build/plugins"],
"loader": {
"resolve": "lazy",
"visibility": "local"
}
}
}
}
}
Current loader options:
resolve:lazyornowvisibility:localorglobal
Defaults are lazy and local.
Building Dynamic Modules
For the producer workflow, metadata embedding, and zynx meta usage, see Building Dynamic Modules.
Current Limitations
- Dynamic modules are currently implemented for Linux and macOS. Windows
.dllloading is not implemented yet. - Linux uses a real
.zynx_metaELF section. - macOS supports real
__DATA,__zynx_metasections when metadata is linked in at build time; post-link embedding still uses trailer transport. - Exported generic callables are rejected for dynamic ABI use.
- Dynamic modules are intended for runtime extension points, not the default packaging model.
Static linking remains the default model for core application builds.
See also: