Skip to main content

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.v1 metadata.
  • Runtime module and symbol loading is lazy and cached.
  • Load failures and symbol-resolution failures use normal Zynx try / catch flow.
  • 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.v1 metadata.
  • 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, or type declarations 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 like T!.
  • 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:

  1. Relative to the source file containing the require(...)
  2. The process working directory
  3. zynx.native.dynamic.searchPaths from zynx.json
  4. CLI --lib-path directories

Project configuration:

{
"zynx": {
"native": {
"dynamic": {
"searchPaths": ["plugins", "build/plugins"],
"loader": {
"resolve": "lazy",
"visibility": "local"
}
}
}
}
}

Current loader options:

  • resolve: lazy or now
  • visibility: local or global

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 .dll loading is not implemented yet.
  • Linux uses a real .zynx_meta ELF section.
  • macOS supports real __DATA,__zynx_meta sections 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: