星光 · 进程外 Provider
星光框架的进程外插件 Provider:行分隔 JSON-RPC 2.0,附带 Node/QuickJS/Python/Java/Kotlin 绑定
- Updated
README0.2.4
Hosihikari.PluginProvider.Rpc
Exception first-class provider. RpcProvider is an official provider: the one exception to the source-AOT-to-IL rule (ADR-0031). Use it when a plugin cannot be compiled to IL.
The exception first-class cross-process bridge of the Hosihikari framework. Plugins run as child processes and speak newline-delimited JSON-RPC 2.0 over stdio; the bridge reads only the command in the manifest and the protocol. Unload means the process really exits.
It is a plugin package itself, named hosihikari-rpc. When it loads, it registers itself with
the framework from OnLoad; packages with provider: "rpc" are only handled after that — which is
why bridge plugins declare a dependency on it.
Templates
RpcPluginTemplate carries complete packages for three runtimes:
What a plugin package looks like
plugins/Hello/
manifest.json
{
"name": "Hello",
"entry": "main.mjs",
"type": "hosihikari",
"version": "1.0.0",
"dependencies": [
{ "name": "hosihikari" },
{ "name": "hosihikari-rpc" }
],
"extraInfo": {
"api": "2",
"format_version": "1",
"provider": "rpc",
"command": "[\"node\", \"./hosihikari.mjs\", \"./main.mjs\"]"
}
}
hosihikari.mjs ← the binding, copied from bindings/node in this repository
main.mjs
Hello.conf
extraInfo.command is a non-empty JSON array encoded as a string — the parent's manifest parser
requires extraInfo values to be strings. The Provider decodes it and passes the items into the
argument list one by one; there is no shell, and the working directory is the package directory. The
runtime executable is supplied by the operator (PATH or an absolute path). Under Endstone, the CLI
sets the child's PATH to Python's sys.path, so name the runtime by absolute path there.
How a plugin is written
The binding connects a default-exported object to the protocol. Hooks may be async; undefined /
true allow, false or { allowed: false, reason } refuse, and a thrown exception is a fault.
import fs from 'node:fs';
import path from 'node:path';
export default {
onLoad(ctx) {
fs.writeFileSync(path.join(ctx.dataDirectory, 'body.txt'), 'body:' + ctx.config('greeting'));
ctx.subscribe('ping', (json) => ctx.logInfo('ping:' + json));
},
onEnable(ctx) { },
onDisable(ctx) { return { allowed: false, reason: 'not yet' }; },
onUnload(ctx) { },
};
ctx exposes name, dataDirectory, packageDirectory, config(key), configObject,
logInfo/logWarn/logError, writeData, publish, subscribe(topic, fn, { sync }), and
runLater/runEvery.
Protocol
- Handshake: the host sends
hosihikari/hellowith the protocol version, name, data directory, package directory and a configuration snapshot; the binding callsonLoad(ctx)before replying. - Host to plugin:
hosihikari/enable,disable,unload,events/deliver. - Plugin to host:
hosihikari/events/publish,subscribe,unsubscribe,log. - Events: asynchronous by default (the host sends a notification and the publisher does not
wait); a subscription can declare
sync: true, in which case the host waits for the ack and treats a timeout as a fault. - Deadlines: waiting for the write gate, writing, and waiting for the response share one timeout. A timeout terminates the child to release blocked pipe writes; notifications and replies have a five-second write timeout. Subscription changes follow their receive order.
- Logging: stdout carries protocol only; stderr is text logging; structured levels go through
hosihikari/log. - Unload: send
unload, close stdin, wait 5 seconds, then kill the process tree. The release verifier checks that the process has exited.
Capability surface
The child process's standard library plus host services. There is no arbitrary .NET interop; the type-keyed service registry does not cross the process boundary (there are no shared types). The data directory is read and written directly; timers belong to the child process.
Running it
dotnet run --project host/Hosihikari.PluginProvider.Rpc.Host.csproj -c Release
It needs sibling Hosihikari.PluginManager, Hosihikari.Abstractions and McMaster fork checkouts,
and Node, qjs, Python (python3; on Windows python or py -3), JDK 21 and kotlinc on PATH.
It covers: the Provider is registered by a plugin package; the Node, QuickJS, Python, Java and
Kotlin bindings each verify the hello snapshot with layered configuration, enable/disable/unload
hooks, asynchronous events by default, the sync ack, timers and publishing back to the host; Java
also covers refused enable/disable and faulting out a process that dies outside a hook, and Kotlin
verifies the same JVM binding from Kotlin.
Bindings
bindings/node, bindings/quickjs, bindings/python and bindings/java hold the reference
bindings, each with test fixtures; bindings/kotlin holds the Kotlin fixture for the JVM binding.
The QuickJS binding needs qjs (quickjs-ng) on PATH or carried in the package; copy
hosihikari.js and stdin_worker.js next to the entry and point the command at it:
"command": "[\"qjs\", \"./hosihikari.js\", \"./main.js\"]"
Its ctx writes through ctx.writeData; files and timers come from the child process's standard
library (qjs:std / qjs:os), and print / console go to stderr.
The Python binding copies hosihikari.py next to the entry; the plugin defines module-level
on_load(ctx) / on_enable(ctx) / on_disable(ctx) / on_unload(ctx):
"command": "[\"python3\", \"./hosihikari.py\", \"./main.py\"]"
ctx provides name, data_directory, package_directory, config(key), config_object,
log_info/log_warn/log_error, write_data(file, text), publish(topic, json),
subscribe(topic, fn, sync=False), run_later(seconds, fn) and run_every(seconds, fn).
print goes to stderr; stdout is protocol only.
The JVM binding lives in bindings/java/hosihikari and serves Java and Kotlin plugins. A plugin
implements hosihikari.Plugin (onLoad / onEnable / onDisable / onUnload); hooks return
HookResult.allow() / HookResult.refuse(reason). Context provides name(), dataDirectory(),
packageDirectory(), config(key), configObject(), logInfo/logWarn/logError,
writeData(file, text), publish(topic, json), subscribe(topic, Consumer<String>[, sync]),
runLater(millis, Runnable) and runEvery(millis, Runnable). Compile the Java plugin together
with the binding:
"command": "[\"java\", \"-cp\", \"./classes\", \"hosihikari.Binding\", \"hello.HelloPlugin\"]"
A Kotlin plugin implements the same interface; compile it against the binding classes and bundle
the Kotlin stdlib (-include-runtime). The runtime classpath separator is : on Unix and ; on
Windows:
"command": "[\"java\", \"-cp\", \"./plugin.jar:./classes\", \"hosihikari.Binding\", \"hello.HelloPlugin\"]"
The JVM binding needs JDK 21; Kotlin plugins additionally need kotlinc at build time, while the
runtime only needs java and the packaged jar. Other languages follow on demand; the protocol
does not change.
The process supervision and subscription ordering regressions can also run without Node, QuickJS, Python, a JDK or Kotlin:
dotnet run --project test/Hosihikari.PluginProvider.Rpc.Regression.csproj -c Release
This check links the production connection source and sibling Abstractions source. It covers blocked writes, shared request deadlines, concurrent disposal, notification timeouts, subscription ordering, and reentrant synchronous event publication. CI runs it alongside the binding assertions.
Packaging
pwsh ./pack.ps1 -Version 0.2.4 -OutputRoot artifacts
It assembles the provider DLL, its private dependencies, the manifest and bindings/ into
hosihikari-rpc/; the shared layer is not copied. Pushing a v* tag attaches the zip to the
GitHub release (ADR-0036).
The script invokes the core packager (Hosihikari.PluginManager/tools/Pack), which owns the
shared-layer list. Check out the core repository alongside this one, or set
HOSIHIKARI_PLUGINMANAGER.
After release: lip install github.com/HosihikariFramework/RpcProvider (install the Shim variant
for the carrier first).
Licence
LGPL-3.0-or-later, see LICENSE.md.