modularize docs
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
# Legacy Native Plugins
|
||||
|
||||
Legacy native v1 plugins run as shared libraries inside the agent. They are still supported and still useful when you need capabilities that the WASM runtime intentionally does not expose.
|
||||
|
||||
Use legacy native plugins when you need direct OS APIs, native libraries, custom process/thread behavior, platform-specific integrations, or code that cannot reasonably be compiled to the Plugin 2.0 WASM ABI.
|
||||
|
||||
Native plugins run with the same privileges as the agent. They are not sandboxed.
|
||||
|
||||
## What Legacy Plugins Can Do
|
||||
|
||||
| Capability | Notes |
|
||||
|------------|-------|
|
||||
| Call native OS APIs | Win32, POSIX, Objective-C/C APIs, platform SDKs, and local native libraries. |
|
||||
| Use language runtimes | C, C++, Go, Rust, and other languages that can export a C ABI. |
|
||||
| Run platform-specific code | Package separate binaries per OS/architecture. |
|
||||
| Emit plugin events | Send events back to the UI/server through the host callback. |
|
||||
| Receive UI/server commands | Handle `PluginOnEvent` calls from plugin UI actions or server-side routing. |
|
||||
|
||||
WASM Plugin 2.0 is preferred for sandboxed, portable agent modules. Legacy native plugins are the escape hatch for privileged native behavior.
|
||||
|
||||
## Bundle Layout
|
||||
|
||||
A legacy bundle is a zip with web assets plus one or more platform binaries:
|
||||
|
||||
```text
|
||||
<pluginId>.zip
|
||||
<pluginId>.html
|
||||
<pluginId>.css
|
||||
<pluginId>.js
|
||||
<pluginId>-windows-amd64.dll
|
||||
<pluginId>-linux-amd64.so
|
||||
<pluginId>-darwin-arm64.dylib
|
||||
```
|
||||
|
||||
The browser assets provide the plugin UI. The native binary is sent to a matching client when the plugin is loaded for that client.
|
||||
|
||||
## Binary Naming
|
||||
|
||||
Binary filenames must follow:
|
||||
|
||||
```text
|
||||
<pluginId>-<os>-<arch>.<ext>
|
||||
```
|
||||
|
||||
| OS | Arch | Example |
|
||||
|----|------|---------|
|
||||
| `windows` | `amd64` | `sample-windows-amd64.dll` |
|
||||
| `windows` | `arm64` | `sample-windows-arm64.dll` |
|
||||
| `linux` | `amd64` | `sample-linux-amd64.so` |
|
||||
| `linux` | `arm64` | `sample-linux-arm64.so` |
|
||||
| `darwin` | `amd64` | `sample-darwin-amd64.dylib` |
|
||||
| `darwin` | `arm64` | `sample-darwin-arm64.dylib` |
|
||||
|
||||
The server never sends an x64 native binary to an ARM client, or the reverse. If no binary matches the target client, loading fails for that client.
|
||||
|
||||
## Required ABI
|
||||
|
||||
Every native plugin exports C-callable functions. The ABI differs slightly between Windows and Unix-like systems.
|
||||
|
||||
| Export | Windows signature | Linux/macOS signature | Required |
|
||||
|--------|-------------------|-----------------------|----------|
|
||||
| `PluginOnLoad` | `int PluginOnLoad(char* hostInfo, int hostInfoLen, uint64 callback)` | `int PluginOnLoad(char* hostInfo, int hostInfoLen, uintptr callback, uintptr ctx)` | Yes |
|
||||
| `PluginOnEvent` | `int PluginOnEvent(char* event, int eventLen, char* payload, int payloadLen)` | Same | Yes |
|
||||
| `PluginOnUnload` | `void PluginOnUnload()` | Same | Yes |
|
||||
| `PluginSetCallback` | `void PluginSetCallback(uint64 callback)` | Not used | Windows only |
|
||||
| `PluginGetRuntime` | `const char* PluginGetRuntime()` | Same | Recommended |
|
||||
|
||||
Return `0` from `PluginOnLoad` and `PluginOnEvent` for success. Return a non-zero value when the plugin cannot initialize or handle the event.
|
||||
|
||||
## HostInfo JSON
|
||||
|
||||
`PluginOnLoad` receives host metadata as a JSON byte buffer:
|
||||
|
||||
```json
|
||||
{
|
||||
"clientId": "abc123",
|
||||
"os": "windows",
|
||||
"arch": "amd64",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
Use this to branch by client OS, architecture, client ID, or agent version.
|
||||
|
||||
## Host Callback
|
||||
|
||||
The host callback lets a native plugin emit events back to the agent/server.
|
||||
|
||||
Windows uses `__stdcall`:
|
||||
|
||||
```c
|
||||
void __stdcall callback(const char *event, uintptr_t eventLen,
|
||||
const char *payload, uintptr_t payloadLen);
|
||||
```
|
||||
|
||||
Linux/macOS use a normal C function plus the opaque context pointer received by `PluginOnLoad`:
|
||||
|
||||
```c
|
||||
void callback(uintptr_t ctx,
|
||||
const char *event, int eventLen,
|
||||
const char *payload, int payloadLen);
|
||||
```
|
||||
|
||||
The event name and payload are byte buffers. JSON payloads are conventional, but the ABI only requires bytes plus lengths.
|
||||
|
||||
## Minimal C Skeleton
|
||||
|
||||
```c
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#define EXPORT __declspec(dllexport)
|
||||
typedef void (__stdcall *host_callback_t)(
|
||||
const char *event, uintptr_t eventLen,
|
||||
const char *payload, uintptr_t payloadLen);
|
||||
static host_callback_t g_callback = 0;
|
||||
#else
|
||||
#define EXPORT __attribute__((visibility("default")))
|
||||
typedef void (*host_callback_t)(
|
||||
uintptr_t ctx,
|
||||
const char *event, int eventLen,
|
||||
const char *payload, int payloadLen);
|
||||
static host_callback_t g_callback = 0;
|
||||
static uintptr_t g_ctx = 0;
|
||||
#endif
|
||||
|
||||
EXPORT const char *PluginGetRuntime(void) {
|
||||
return "c";
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
EXPORT void PluginSetCallback(uint64_t callback) {
|
||||
g_callback = (host_callback_t)(uintptr_t)callback;
|
||||
}
|
||||
|
||||
EXPORT int PluginOnLoad(const char *hostInfo, int hostInfoLen, uint64_t callback) {
|
||||
g_callback = (host_callback_t)(uintptr_t)callback;
|
||||
if (g_callback) g_callback("ready", 5, "{\"ok\":true}", 11);
|
||||
return 0;
|
||||
}
|
||||
#else
|
||||
EXPORT int PluginOnLoad(const char *hostInfo, int hostInfoLen, uintptr_t callback, uintptr_t ctx) {
|
||||
g_callback = (host_callback_t)callback;
|
||||
g_ctx = ctx;
|
||||
if (g_callback) g_callback(g_ctx, "ready", 5, "{\"ok\":true}", 11);
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
EXPORT int PluginOnEvent(const char *event, int eventLen, const char *payload, int payloadLen) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
EXPORT void PluginOnUnload(void) {
|
||||
}
|
||||
```
|
||||
|
||||
## Runtime Detection
|
||||
|
||||
`PluginGetRuntime` should return a pointer to a static, null-terminated string.
|
||||
|
||||
| Return value | Meaning |
|
||||
|--------------|---------|
|
||||
| `c` | C plugin; fully unloadable. |
|
||||
| `cpp` | C++ plugin; fully unloadable. |
|
||||
| `rust` | Rust plugin; fully unloadable when written without unload-hostile globals. |
|
||||
| `go` or absent | Go runtime; intentionally not unloaded. |
|
||||
|
||||
Go shared libraries cannot be fully unloaded safely because the Go runtime owns threads and process-global state. The host treats Go plugins as non-freeable to avoid crashes.
|
||||
|
||||
## Loading Model
|
||||
|
||||
| Platform | Loading behavior |
|
||||
|----------|------------------|
|
||||
| Windows | DLLs are loaded from memory through the custom PE loader. |
|
||||
| Linux | Shared libraries are loaded through the static-agent subprocess shim when needed. |
|
||||
| macOS | Dynamic libraries are loaded through the platform loader. |
|
||||
|
||||
Because Windows uses an in-memory PE loader, some runtime primitives that assume normal `LoadLibrary` initialization can be fragile. The Rust sample avoids `std::sync::Mutex` for this reason and uses C-style globals; the host serializes plugin calls.
|
||||
|
||||
## Static Linux Agent Shim
|
||||
|
||||
Linux agents are compiled as static musl binaries for portability. Static binaries cannot directly call `dlopen`, so Linux native plugin support uses an embedded plugin-host shim.
|
||||
|
||||
When loading a Linux native plugin, the agent:
|
||||
|
||||
1. Writes the plugin `.so` to an anonymous in-memory file.
|
||||
2. Writes the embedded shim to another in-memory file.
|
||||
3. Creates a bidirectional socket.
|
||||
4. Starts the shim and passes file descriptors to it.
|
||||
5. The shim loads the `.so` and forwards events over the socket.
|
||||
|
||||
Plugin authors normally do not need shim-specific code. Export the normal v1 ABI and compile the `.so` for the target libc/architecture.
|
||||
|
||||
## IPC Protocol Notes
|
||||
|
||||
The agent and Linux shim use a length-prefixed binary protocol:
|
||||
|
||||
```text
|
||||
[4-byte little-endian total payload length][1-byte message type][payload bytes]
|
||||
```
|
||||
|
||||
Important message types:
|
||||
|
||||
| Direction | Type | Meaning |
|
||||
|-----------|------|---------|
|
||||
| agent to shim | `0x01` | Load plugin with HostInfo. |
|
||||
| agent to shim | `0x02` | Deliver plugin event. |
|
||||
| agent to shim | `0x03` | Unload plugin. |
|
||||
| shim to agent | `0x10` | Plugin callback event. |
|
||||
| shim to agent | `0x11` | Ready/runtime string. |
|
||||
| shim to agent | `0x12` | Error string. |
|
||||
| shim to agent | `0x13` | Load result. |
|
||||
|
||||
Most plugin authors can ignore this, but it is useful when debugging Linux native plugin load failures.
|
||||
|
||||
## Build Scripts
|
||||
|
||||
Legacy samples include `build.bat` and `build.sh`. They compile native binaries and zip them with web assets.
|
||||
|
||||
Default usage:
|
||||
|
||||
```bash
|
||||
cd plugins/sample-c
|
||||
./build.sh
|
||||
```
|
||||
|
||||
Multiple targets:
|
||||
|
||||
```bash
|
||||
BUILD_TARGETS="linux-amd64 linux-arm64 windows-amd64 darwin-arm64" ./build.sh
|
||||
```
|
||||
|
||||
Windows:
|
||||
|
||||
```bat
|
||||
cd plugins\sample-c
|
||||
build.bat
|
||||
```
|
||||
|
||||
The scripts support `BUILD_TARGETS` and usually accept an optional plugin directory argument.
|
||||
|
||||
## Cross-Compilation
|
||||
|
||||
Install the compiler matching the target platform, or set `CC` for C/C++ samples.
|
||||
|
||||
Common compiler names used by the sample scripts:
|
||||
|
||||
| Target | Compiler |
|
||||
|--------|----------|
|
||||
| `linux-amd64` | `x86_64-linux-gnu-gcc` |
|
||||
| `linux-arm64` | `aarch64-linux-gnu-gcc` |
|
||||
| `linux-arm` | `arm-linux-gnueabihf-gcc` |
|
||||
| `windows-amd64` | `x86_64-w64-mingw32-gcc` |
|
||||
| `windows-arm64` | `aarch64-w64-mingw32-gcc` |
|
||||
| `darwin-amd64` | `x86_64-apple-darwin-gcc` |
|
||||
| `darwin-arm64` | `aarch64-apple-darwin-gcc` |
|
||||
|
||||
Rust samples use Cargo targets. Go samples use `go build -buildmode=c-shared` with CGO enabled.
|
||||
|
||||
## Language Notes
|
||||
|
||||
| Sample | Notes |
|
||||
|--------|-------|
|
||||
| `sample-c` | Smallest ABI surface, no runtime, fully unloadable. |
|
||||
| `sample-cpp` | C++ convenience with exported C ABI. Return `cpp` from `PluginGetRuntime`. |
|
||||
| `sample-rust` | Exports a C ABI. Avoid assumptions that require normal OS loader initialization. |
|
||||
| `sample-go` | Uses `-buildmode=c-shared`; treated as non-freeable because the Go runtime cannot be safely unloaded. |
|
||||
|
||||
## UI Event Flow
|
||||
|
||||
The browser UI sends events through the normal plugin event endpoint. The server forwards them to the client. The agent calls:
|
||||
|
||||
```text
|
||||
PluginOnEvent(event, eventLen, payload, payloadLen)
|
||||
```
|
||||
|
||||
The native plugin can reply by invoking the host callback. Those callback events are delivered back to the server/UI as plugin events.
|
||||
|
||||
## Security Notes
|
||||
|
||||
Legacy native plugins are trusted executable code. They can call OS APIs, read/write files available to the agent process, spawn subprocesses, open sockets, and interact with native libraries. Use plugin signing, per-user plugin access, and code review before deploying third-party native plugins.
|
||||
Reference in New Issue
Block a user