Native Plugins

Native plugins are shared libraries that register C++ functions and modules
directly into the scripting namespace. They are the fastest way to extend
Scrii with host functionality.

Scaffolding a plugin

The module/ folder ships a generator:

./module/create_module.sh mymod
make -C mymod               # -> mymod.so / .dylib / .dll
../build/interpreter/scrii_repl mymod/example.scr

This creates:

mymod/
├── mymod.cpp        plugin source — implements scrii_plugin_init()
├── include/         copies of the scrii headers it compiles against
│   ├── plugin.hpp          Registry interface + ABI version
│   ├── parser/result.hpp   status enum, result type
│   ├── parser/error.hpp    ScriptError for argument validation
│   └── var/var.hpp         var::Var value type
├── Makefile         -> .so / .dylib / .dll
└── example.scr      load_plugin(...) demo

The generated mymod.cpp registers a restricted module with one sample
function. Edit it, add your own functions to the module, and rebuild.

The plugin entry point

#include "plugin.hpp"

// Checked by the host BEFORE init() runs: a mismatched plugin is refused
// without executing any of its code.
extern "C" int scrii_plugin_abi_version() {
  return scrii::plugin::kAbiVersion;
}

extern "C" bool scrii_plugin_init(scrii::plugin::Registry &reg) {
  if (reg.abi_version() != scrii::plugin::kAbiVersion)
    return false;

  reg.add_function("shout",
    [](std::vector<scrii::var::Var> a) -> scrii::var::Var {
      return scrii::var::Var(a[0].to_string() + "!!!",
                             scrii::var::var_type::STRING);
    },
    "shout(s) - Uppercase s with emphasis");

  reg.add_module("mymod", "my module description", { ... });   # optional restricted module object

  return true;
}

Build with the same compiler and headers as the host interpreter. The plugin
does not link against scrii — the host exports the symbols that the plugin
resolves against, which is why the generated Makefile has no link step.

The Registry API

The scrii::plugin::Registry passed to scrii_plugin_init is the plugin’s
whole interface to the interpreter:

Member Description
int abi_version() Returns kAbiVersion. Refuse to load if it doesn’t match what the plugin was compiled against.
const std::string &engine_version() The host engine’s version string.
void add(name, var::Var value) Registers value as a restricted top-level global. Re-registering replaces the previous value.
void add_function(name, fn, description) Convenience wrapper for add registering a function global.
void add_module(name, description, members) Registers a restricted module object holding the given member table.
std::vector<std::string> registered_names Names registered through this Registry (used for unload bookkeeping).

module in the sample above is built with add_module:

extern "C" bool scrii_plugin_init(scrii::plugin::Registry &reg) {
  if (reg.abi_version() != scrii::plugin::kAbiVersion)
    return false;

  reg.add_module("mymod", "My demo module.", {
    {"greet", scrii::var::Var(
      [](std::vector<scrii::var::Var> a) -> scrii::var::Var {
        return scrii::var::Var("hi " + a[0].to_string(),
                               scrii::var::var_type::STRING);
      },
      scrii::var::var_type::FUNCTION)},
  });

  return true;
}
load_plugin("./mymod.so")
print(mymod.greet("world"))   # hi world

The ABI version is the one stable contract between host and plugin. Bump your
plugin’s compiled headers in lockstep with the host. There are two layers of
checking:

  1. Host-side gate (hard): if the plugin exports
    scrii_plugin_abi_version() — the scaffolding generates it — the host
    compares it before calling init() and refuses mismatched plugins
    without running any plugin code.
  2. Plugin-side check (backstop): the reg.abi_version() comparison
    inside init() covers older plugins that predate the exported symbol.

A refused plugin surfaces as PLUGIN_LOAD_FAILED naming both ABI versions;
rebuild the plugin against the host’s headers to fix it.

Loading from a script

load_plugin("./mymod.so")   # true on success
print(shout("hi"))          # HI!
  • Registered names become restricted globals — scripts can call them but
    never overwrite them.
  • Duplicate loads of the same file are no-ops.
  • unload_plugin(path) removes the module’s globals immediately; guarded
    calls then throw "plugin module is unloaded". The library itself stays
    mapped until engine teardown (deferred close), and re-loading works
    normally afterwards.

Platforms

Platform Generator Build command Output
Linux bash make .so
macOS bash make .dylib
Windows Git Bash / WSL mingw32-make (MinGW) .dll

MSVC one-liner equivalent:

cl /std:c++23 /LD /EHsc /Iinclude mymod.cpp /Fe:mymod.dll

Gotchas

  • Keep the plugin’s compiler/headers matching the host binary’s toolchain —
    C++ ABIs are not stable across compilers or versions.
  • Throw scrii::ScriptError{status::..., msg} for clean script-visible
    errors; other exceptions are caught and surfaced too.
  • The embeddable core provides the same plugin.hpp interface — see
    Embedding.