Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
polyplug-guest — Rust Guest Library
The polyplug_guest crate provides the types, constants, and allocator helpers that
plugin authors need to implement a polyplug
contract in Rust.
Who is this for? You are writing a shared library (
.so/.dylib/.dll) that will be loaded by a polyplug host at runtime. You are not writing a host application — seecrates/polyplug/for that.
Table of Contents
- Quick Start
- Cargo.toml Setup
- Available Types
- ABI Constants
- Hash Utilities
- Allocator API
- Implementing a Contract — Step by Step
- Full Example
- manifest.toml — Declaring Your Bundle
- Building
- Error Handling
- Memory Rules
- More Examples
Quick Start
Add polyplug_guest to your plugin's Cargo.toml, set crate-type = ["cdylib"], then
export two symbols: polyplug_abi_version and polyplug_init.
# Cargo.toml
[]
= "my_plugin"
= "1.0.0"
= "2024"
[]
= ["cdylib"]
[]
= { = true }
= { = true }
= { = true }
// src/lib.rs
use *;
use FnPtr;
use GuestContractId;
// FNV-1a of "guest_contract:my.contract@1" — see polyplug_utils::guest_contract_id
const MY_CONTRACT_ID: u64 = 0x3AFC01CA348E3F0D;
extern "C"
static MY_FNS: = ;
static MY_VTABLE: GuestContractInterface = GuestContractInterface ;
static MY_DESCRIPTOR: PluginDescriptor = PluginDescriptor ;
pub extern "C"
pub unsafe extern "C"
Cargo.toml Setup
Your plugin must be a cdylib:
[]
= ["cdylib"]
Important: A plugin cdylib must never depend on the host-side polyplug
crate — the host already embeds polyplug, and a plugin only needs the ABI surface.
Depend on polyplug_abi (the #[repr(C)] ABI types), polyplug_guest (guest-side
helpers), and polyplug_utils (ID hashing), as the workspace examples under
examples/guests/rust/ do. A plugin distributed outside this workspace can instead
mirror the #[repr(C)] ABI types inline — the ABI is defined by struct layout, not
by linkage.
Available Types
The ABI types live in the polyplug_abi crate — import them from there directly
(use polyplug_abi::*; or selectively). polyplug_guest adds the guest-side helpers
(FnPtr, GuestError, HostContext, to_str, …); it does not re-export
polyplug_abi (the workspace is cross-crate-re-export free).
StringView
Non-owning, non-null-terminated UTF-8 string reference.
Ownership: Borrowed. ptr must remain valid for the duration of the call. The
receiver must not free it.
// Create from a Rust string slice (valid for the call's lifetime):
let s: &str = "hello";
let sv = StringView ;
// Create a null view (empty / absent):
let null_sv: StringView = null;
Buffer
Owning byte buffer allocated via the host allocator.
Ownership: The holder must free with host.free(host, ptr, cap, 1) when done.
Never place Buffer data on the Rust allocator — always allocate through the host's
HostApi (host.alloc, e.g. via HostContext::alloc_string).
AbiError
Returned by value from all ABI calls.
code == 0→ success (AbiErrorCode::Ok).code != 0→ failure;messageprovides a human-readable description.- Ownership of
message.ptr: always a static or runtime-owned string. The receiver never frees it (a bareStringViewcarries no allocation provenance). For rich, allocated error detail, useget_last_errorinstead.
Convenience constructors:
ok // AbiError { code: 0, message: StringView::null() }
GuestContractInterface
One per contract your plugin implements. Must be 'static.
contract_idis the FNV-1a 64-bit hash of"guest_contract:name@major_version"(compute it withpolyplug_utils::guest_contract_id, wrap it withGuestContractId::from_u64).dispatch_typedetermines how to access thedispatchunion:Native— usedispatch.native.functions[fn_id]for direct function pointer calls.VirtualMachine— usedispatch.vm.call(loader_data, instance, fn_id, args, out).
create_instance/destroy_instancemanage instance lifecycle (required for hot-reload).
PluginDescriptor
Metadata about your plugin. Must be 'static.
HostApi
Passed by the host to your polyplug_init and to every create_instance call. The
pointer stays valid for the plugin's lifetime. The generated glue wraps it in a
polyplug_guest::HostContext and hands it to your author factory
(polyplug_create_<plugin>); store that context in your implementation struct for
allocation, logging, and cross-contract calls. There is NO process-wide host storage
in this SDK — the pointer always flows through instances.
All functions use the self-passing pattern — the first parameter is always the
HostApi pointer itself. Call (iface.register_guest_contract)(host, &MY_DESCRIPTOR, &MY_VTABLE) for each contract
your plugin implements.
GuestContractHandle
Opaque handle to another loaded plugin. Validated by the host on every use.
A zeroed GuestContractHandle ({ index: 0, generation: 0 }) is the sentinel "not found" value.
BundleInitContext
Passed to polyplug_init as the second argument. Contains bundle_id (the FNV-1a
hash of the bundle name) and bundle_path — the absolute path to the bundle
directory on disk (24 bytes total).
Do not store the raw pointer. Copy the string value if you need it after init.
FnPtr
Wrapper around *const () that implements Sync and Send, enabling function pointers
in static arrays.
// Build your vtable function array:
static MY_FNS: = ;
GuestError
High-level error type for use in generated trait implementations.
Implements std::error::Error and Display. Returned from generated wrapper code when
an ABI call returns a non-zero code.
ABI Error Codes
The AbiErrorCode enum defines all possible return codes:
Use AbiErrorCode::Ok for success, AbiErrorCode::Generic for errors.
Hash Utilities
ID hashing lives in the polyplug_utils crate. Guest contract IDs are FNV-1a 64-bit
hashes of "guest_contract:name@major_version" (the prefix keeps guest and host
contract IDs from colliding). polyplugc generates the constants for you; use
guest_contract_id() at runtime to verify.
use guest_contract_id;
// Verify the compile-time constant matches the runtime hash:
assert_eq!;
| Function | Description |
|---|---|
guest_contract_id(name: &str, major_version: u32) -> u64 |
FNV-1a of "guest_contract:name@major" |
host_contract_id(name: &str, major_version: u32) -> u64 |
FNV-1a of "host_contract:name@major" |
bundle_id(name: &str) -> u64 |
FNV-1a of the bundle name |
fnv1a_64(data: &[u8]) -> u64 |
Raw FNV-1a 64-bit hash |
Allocator API
All memory that crosses the plugin/host boundary must use the host allocator,
reached through the alloc / free function-pointer fields of the HostApi your
implementation received via its HostContext. There are no polyplug_host_alloc /
polyplug_host_free C exports — allocation always flows through that interface.
Never return heap-allocated data from your Rust allocator — the host cannot free it.
For strings, prefer the HostContext::alloc_string method, which allocates through
host.alloc:
// `self.host` is the HostContext your factory received and stored.
let sv: StringView = self.host.alloc_string?;
// sv.ptr points to host-allocated memory; the host frees it via
// host.free(host, sv.ptr, sv.len, 1) when done.
For raw buffers, call the interface directly through the context's raw pointer:
let host: *const HostApi = self.host.as_ptr;
// SAFETY: the HostContext came from create_instance and is valid for the
// plugin's lifetime.
let ptr: *mut u8 = unsafe ;
if ptr.is_null
// Free it through the same interface:
unsafe ;
Rules:
- A plugin must never free memory it did not allocate.
- Never place cross-boundary data on the Rust managed heap (
Box,Vec,String). - For string outputs, allocate via
HostContext::alloc_string(orhost.alloc), copy bytes in, return aStringViewpointing to that allocation.
Implementing a Contract — Step by Step
Step 1: Compute the contract ID
polyplugc generates the constant for you. For hand-written plugins:
// FNV-1a-64 of "guest_contract:pipeline.transformer@1"
const TRANSFORMER_CONTRACT_ID: u64 = 0x1F7F345779EDC6DC;
Verify at startup with polyplug_utils::guest_contract_id("pipeline.transformer", 1).
Step 2: Define your ABI types
Declare #[repr(C)] structs matching the contract's argument and return types exactly.
Step 3: Implement your function with the generic dispatch signature
Every function in the vtable has the signature extern "C" fn(*const (), *mut ()) -> AbiError.
Cast the opaque pointers to your concrete types inside.
extern "C"
Step 4: Declare static vtable and descriptor
static TRANSFORM_FNS: = ;
static TRANSFORM_VTABLE: GuestContractInterface = GuestContractInterface ;
static TRANSFORM_DESCRIPTOR: PluginDescriptor = PluginDescriptor ;
Step 5: Export the two mandatory ABI symbols
pub extern "C"
pub unsafe extern "C"
Full Example
The canonical Rust guest plugins live under
examples/guests/rust/ — decoder, transformer,
validator, encoder, and reporter. For instance,
examples/guests/rust/transformer/src/lib.rs
implements the data.Transformer contract by implementing the
polyplugc-generated trait and registering it through the generated polyplug_init.
Key points demonstrated:
- Generated bindings:
polyplugc generateemitsgenerated/guest/(trait, vtable,polyplug_init) andgenerated/manifest.toml - Safe UTF-8 decoding of
StringViewdata viapolyplug_guest::to_str - Host-allocator string returns via
polyplug_guest::HostContext::alloc_string - Proper error returns (
Result<_, GuestError>) instead of panics // SAFETY:comments on everyunsafeblock
examples/guests/rust/transformer/
├── Cargo.toml # crate-type = ["cdylib"]; polyplug_abi + polyplug_guest + polyplug_utils deps
├── bundle.toml # contract definition consumed by polyplugc
├── generated/ # polyplugc output: guest bindings + manifest.toml — do not edit
└── src/
└── lib.rs # the hand-written part: the trait implementation
manifest.toml — Declaring Your Bundle
Every plugin bundle needs a manifest.toml alongside the compiled .so.
polyplugc generate emits it for you (under generated/); a hand-written one
looks like this:
= "my_plugin"
= 6083502456968126439 # fnv1a_64("my_plugin"); polyplugc precomputes this
= "1.0.0"
= "native"
= ["pipeline.transformer@1"]
= { = 1 }
= "libmy_plugin.so"
file may also be a per-platform table (this is what polyplugc emits):
[]
= "libmy_plugin.so"
| Field | Description |
|---|---|
name |
Unique name for this bundle |
id |
Bundle ID — required, non-zero; polyplugc precomputes it from the name |
version |
Bundle version string |
runtime |
Must be "native" for Rust/C/C++ plugins (required) |
file |
Filename of the compiled shared library, or a per-platform table |
provides |
Contract names this bundle implements, at "name@major" |
function_count |
Map from "name@major" to number of exported functions |
[[dependency]] |
Optional declared dependencies (see docs/TRUST_MODEL.md) |
Building
# Debug build
# Release build (recommended for distribution)
# Copy to your bundle directory
The output is a native shared library. File names by platform:
| Platform | Output file |
|---|---|
| Linux | libmy_plugin.so |
| macOS | libmy_plugin.dylib |
| Windows | my_plugin.dll |
Place the compiled library alongside its manifest.toml in your bundle directory.
Bundle layout
Assemble the bundle directory yourself:
dist/my-plugin/
├── manifest.toml # emitted by `generate` (carries the precomputed bundle_id)
└── libmy_plugin.so # the cdylib you compiled (.dylib on macOS, .dll on Windows; loader = "native")
Validate the assembled directory before shipping:
Error Handling
Never use .unwrap() in plugin code. Return AbiError with a non-zero code instead.
// FORBIDDEN in plugin code
let s = from_utf8.unwrap;
// CORRECT — return an AbiError on failure
let s: &str = match from_utf8 ;
AbiError.message is always a static or runtime-owned string. The receiver
never frees it — a bare StringView carries no allocation provenance, so the
host cannot and does not free message.ptr. Point it at
a 'static byte string, as above. If you need rich, allocated error detail,
expose it through get_last_error rather than through AbiError.message.
AbiError.code is a raw u32, not the AbiErrorCode enum: plugins are
untrusted and may return any 32-bit value. Construct it with
AbiErrorCode::Variant as u32 and interpret a received code with
AbiErrorCode::from_u32.
Memory Rules
| Rule | Detail |
|---|---|
| Cross-boundary allocations | Must use the HostApi alloc / free fields (via HostContext) |
| Plugin-side allocations | May use Rust allocator, but must NOT cross the boundary |
| Returned strings / buffers | Must be allocated via HostContext::alloc_string / host.alloc |
StringView (input) |
Borrowed — do NOT free; valid only for the duration of the call |
Buffer (output) |
Owned — caller frees with host.free(host, ptr, cap, align) |
AbiError.message (output) |
Static or runtime-owned; receiver NEVER frees it |
More Examples
examples/guests/rust/— Rust guest plugins (decoder,transformer,validator,encoder,reporter)examples/guests/— The same guest plugins in C++, C#, Python, Lua, and JavaScriptexamples/hosts/— Host runtimes that load polyplug bundlesexamples/api.toml— API definition used bypolyplugcfor codegen