orbit-plugin-abi

Fixed, auditable native ABI bridge for Orbit plugins.

moonbit
orbit
plugin
native
ffi
moon add Nanaloveyuki/orbit-plugin-abi@0.2.2
Download zip
Version
0.2.2
License
Apache-2.0
Last updated
7 days ago
Downloads
168

Dependencies

README

#orbit-plugin-abi

Nanaloveyuki/orbit-plugin-abi is Orbit's native-only, fixed-signature C ABI bridge. It consumes symbol addresses returned by Nanaloveyuki/dynlib and never exposes a generic call-by-address API.

#Fixed ABI versions

Both ABI versions export exactly these five C functions:

uint32_t orbit_plugin_abi_version(void); const char *orbit_plugin_manifest_json(void); /* The host argument is OrbitHostV1 for ABI 1 and OrbitHostV2 for ABI 2. */ int32_t orbit_plugin_create(const void *host, void **out_instance); int32_t orbit_plugin_invoke( void *instance, const char *command, const uint8_t *request, uint32_t request_len, OrbitBuffer *out_response); void orbit_plugin_destroy(void *instance);

orbit_plugin_manifest_json returns a static, NUL-terminated UTF-8 string. This package copies and validates its UTF-8 but deliberately does not parse JSON. PluginAddresses::from_symbols obtains five addresses from dynlib Symbol values; PluginAddresses::new is also available for a host that already holds validated addresses. Plugin::open accepts ABI 1 and ABI 2 and records the exact reported version.

ABI 1 remains the synchronous compatibility contract. ABI 2 preserves the complete OrbitHostV1 layout prefix and adds struct_size, feature flags, a host context, a synchronous host-request callback, and cooperative invocation cancellation. The five plugin export names and the invoke/destroy signatures do not change.

#ABI 2 executor

ABI 2 plugins must be created with Plugin::start_executor. Each executor owns one native worker and calls create, every invoke, and destroy on that same worker in strict FIFO order. MoonBit commands and requests are copied into a bounded native queue before the submit FFI call returns. Invocation IDs remain reserved until their completion event is polled. The outstanding count and native event bytes are also bounded, so an unresponsive host applies backpressure instead of growing the queue without limit. Completion and host- request events remain native-owned until PluginExecutor::poll_event copies them on the host thread.

The wakeup argument must be a no-capture FuncRef[() -> Unit] that calls only a foreign-thread-safe native wake primitive. It must not inspect MoonBit heap state. The intended integration is to wake a host event loop, poll executor events on the UI/main thread, and signal the async task waiting for that invocation from there.

A v2 plugin may call OrbitHostV2::request only synchronously from the executor worker while one of its orbit_plugin_invoke calls is active. Plugin- created threads must not call either v2 callback or retain host_context for later callback use. The request call copies the command and JSON request into the host event queue and blocks the plugin worker until the host completes, cancels, or shuts down that request. A successful response buffer is allocated by host.alloc; the plugin owns it and must eventually pass it to host.free (or transfer it as its invocation response). timeout_ms == 0 means inherit the outer invocation deadline.

Cancellation is cooperative. OrbitHostV2::invocation_cancelled observes the current invocation's native cancellation flag, and cancellation unblocks a pending host request. Orbit cannot safely preempt arbitrary plugin code. Begin shutdown first, continue polling until Stopped, then call join_stopped before unloading the dynamic library. A host with a synchronous teardown path may instead call begin_shutdown followed by blocking join; trusted plugin code that ignores cooperative cancellation can delay that join indefinitely.

#Ownership and lifetime

ABI 1 create receives a process-static const OrbitHostV1 *. ABI 2 receives an executor-owned const OrbitHostV2 *. A plugin may retain its host pointer until the matching destroy; it must not free or mutate it. host.alloc and host.free define the only supported allocator boundary. Successful invoke responses must be allocated through the host allocator. The bridge copies OrbitBuffer.data into MoonBit memory and calls host.free exactly once.

An empty request is passed as a zero-length request buffer. Commands are UTF-8, non-empty, contain no embedded NUL, and are capped at 1024 bytes. Requests and responses are limited to 16 MiB; manifests are limited to 1 MiB. ABI 1 surfaces non-zero plugin statuses as AbiError::PluginStatus. ABI 2 reports them in ExecutorEvent::InvocationFinished.

ABI 1 destroy is idempotent on the MoonBit side. Keep the originating dynlib library open until every v1 instance is destroyed and every v2 executor has reported Stopped and been joined.

#Safety boundary

This package protects the host from accidental ABI mismatch, null addresses, oversized buffers, invalid command strings, queue overflow, stale completion, and malformed response buffers. It cannot make arbitrary native code safe: a plugin is trusted native code. Plugins must not throw C++ exceptions, panic across the C ABI, retain request or command pointers after invoke returns, or return memory allocated by an allocator other than host.alloc.

#Development

moon fmt --check moon check --target native --deny-warn --warn-list +73 moon test --target native --deny-warn moon info --target native

#
AbiError

pub(all) enum AbiError {
InvalidAddress
IncompatibleAbi(UInt)
ManifestUnavailable
InvalidManifestUtf8
InvalidCommand
RequestTooLarge(Int)
InvalidResponse
PluginStatus(Int)
InstanceUnavailable
Destroyed
ExecutorRequired
ExecutorUnsupported
ExecutorUnavailable
ExecutorQueueFull
DuplicateInvocation
ExecutorClosed
InvocationUnavailable
MalformedEvent
ExecutorStatus(Int)
} derive(Eq,
Debug
)

A failure reported by the fixed Orbit plugin ABI bridge.

#
AbiVersion

pub(all) enum AbiVersion {
V1
V2
} derive(Eq,
Debug
)

#
ExecutorEvent

pub(all) enum ExecutorEvent {
Ready(Int)
InvocationFinished(UInt64, Int, Bytes)
HostRequest(HostRequest)
HostRequestCancelled(UInt64, UInt64, Int)
Stopped
} derive(Eq,
Debug
)

Events produced by a native plugin executor and drained on the host thread.

Status zero means success. A non-zero invocation status is either the plugin's own status or a documented executor bridge status.

#
HostRequest

pub(all) struct HostRequest {
request_id : UInt64
invocation_id : UInt64
command : String
request : Bytes
timeout_ms : UInt
} derive(Eq,
Debug
)

One host IPC request made synchronously by a v2 plugin invocation.

The request and command are copied out of native storage while polling on the host thread. timeout_ms == 0 means inherit the outer invocation deadline.

#
Plugin

pub struct Plugin {
// private fields
}

A validated plugin ABI descriptor with its copied manifest.

#
Plugin::abi_version

fn Plugin::abi_version(self : Plugin) -> AbiVersion

Returns the exact ABI version reported by the plugin.

#
Plugin::create

fn Plugin::create(self : Plugin) -> Result[PluginInstance, AbiError]

Creates one plugin instance with a process-static HostV1. The plugin may retain that host pointer until destroy is called.

#
Plugin::manifest_json

fn Plugin::manifest_json(self : Plugin) -> String

Returns the static plugin manifest copied during open.

#
Plugin::open

fn Plugin::open(addresses : PluginAddresses) -> Result[Plugin, AbiError]

Calls only the fixed ABI-version and manifest signatures to validate a plugin. It copies the manifest and verifies its UTF-8, but does not parse JSON.

#
Plugin::start_executor

fn Plugin::start_executor(self : Plugin, wakeup_callback : FuncRef[() -> Unit]) -> Result[PluginExecutor, AbiError]

Starts a dedicated executor for a v2 plugin.

wakeup_callback must be a no-capture native wake primitive. It may be invoked by the executor thread and must not access MoonBit-managed values.

#
PluginAddresses

pub struct PluginAddresses {
// private fields
}

The five required addresses obtained from Nanaloveyuki/dynlib symbols.

Every address is validated before a native call is attempted. The owning dynamic library must remain open until every PluginInstance is destroyed.

#
PluginAddresses::from_symbols

Obtains the five fixed ABI addresses from published dynlib symbols.

The backing Library must remain open until every PluginInstance is destroyed. A closed source symbol is rejected before any ABI call.

#
PluginAddresses::new

fn PluginAddresses::new(abi_version_address : UInt64, manifest_address : UInt64, create_address : UInt64, invoke_address : UInt64, destroy_address : UInt64) -> Result[PluginAddresses, AbiError]

Constructs the complete v1 symbol set.

Use Symbol::address from the published dynlib package for each address.

#
PluginExecutor

pub struct PluginExecutor {
// private fields
}

A v2 plugin instance and its dedicated native worker.

All five plugin ABI functions execute on that worker. The worker only stores native-owned copies and invokes the provided no-capture wakeup function. Poll events only from the host/UI thread.

#
PluginExecutor::begin_shutdown

fn PluginExecutor::begin_shutdown(self : PluginExecutor) -> Unit

Rejects new work and cooperatively cancels all queued/running work.

#
PluginExecutor::cancel

fn PluginExecutor::cancel(self : PluginExecutor, invocation_id : UInt64) -> Result[Unit, AbiError]

Cancels a queued invocation or marks the running invocation cooperatively cancelled. Arbitrary native plugin code cannot be preempted safely.

#
PluginExecutor::cancel_host_request

fn PluginExecutor::cancel_host_request(self : PluginExecutor, request_id : UInt64, status : Int) -> Result[Unit, AbiError]

Completes a pending host request with a non-zero bridge status.

#
PluginExecutor::complete_host_request

fn PluginExecutor::complete_host_request(self : PluginExecutor, request_id : UInt64, response : Bytes) -> Result[Unit, AbiError]

Completes a pending host request with a copied IPC response envelope.

#
PluginExecutor::is_closed

fn PluginExecutor::is_closed(self : PluginExecutor) -> Bool

#
PluginExecutor::join

fn PluginExecutor::join(self : PluginExecutor) -> Result[Unit, AbiError]

Blocks until the executor worker has destroyed the plugin, then releases all native executor state. Call begin_shutdown first. Arbitrary plugin code remains trusted and can delay this join indefinitely if it ignores cooperative cancellation.

#
PluginExecutor::join_stopped

fn PluginExecutor::join_stopped(self : PluginExecutor) -> Result[Unit, AbiError]

Joins and releases an executor only after its Stopped event was observed.

#
PluginExecutor::poll_event

fn PluginExecutor::poll_event(self : PluginExecutor) -> Result[ExecutorEvent?, AbiError]

Polls one native event. None means the completion queue is empty.

#
PluginExecutor::submit

fn PluginExecutor::submit(self : PluginExecutor, invocation_id : UInt64, command : String, request : Bytes) -> Result[Unit, AbiError]

Submits a copied invocation to the executor's bounded FIFO queue.

#
PluginInstance

pub struct PluginInstance {
// private fields
}

A plugin instance created through the fixed v1 ABI.

Destroying is idempotent. Do not close the source dynlib Library before calling destroy.

#
PluginInstance::destroy

fn PluginInstance::destroy(self : PluginInstance) -> Unit

Destroys the native plugin instance once. Repeated calls are no-ops.

#
PluginInstance::invoke

fn PluginInstance::invoke(self : PluginInstance, command : String, request : Bytes) -> Result[Bytes, AbiError]

Invokes the fixed v1 plugin command signature.

Command is UTF-8 without embedded NUL. Empty requests are passed as a non-null zero-length byte buffer. Successful responses are copied before the host allocator frees the plugin-owned OrbitBuffer.

#
PluginInstance::is_destroyed

fn PluginInstance::is_destroyed(self : PluginInstance) -> Bool

Returns whether the instance has already been destroyed.

#
supported_abi_version

fn supported_abi_version() -> UInt

Returns the supported plugin ABI version.

#
supported_abi_versions

fn supported_abi_versions() -> Array[UInt]

Returns every ABI version accepted by this host bridge.

Powered by MoonBit

Site sourceReport issuePackagesBuild queueSkillsStatistics

© 2026 mooncakes.io