make c, cpp and rust plugins as well as their builders (thank you claude). Also add support for multi lang plugins so that we can actually fucking unload them unlike shitty golang (issue has been opened since 2015 and there is no due date for the issue...)
This commit is contained in:
@@ -45,6 +45,11 @@ static void so_call_onunload(void* fn) {
|
||||
typedef void (*fn_t)(void);
|
||||
((fn_t)fn)();
|
||||
}
|
||||
|
||||
static const char* so_call_getruntime(void* fn) {
|
||||
typedef const char* (*fn_t)(void);
|
||||
return ((fn_t)fn)();
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
@@ -125,20 +130,29 @@ func loadNativePlugin(data []byte) (NativePlugin, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pluginRT := "go"
|
||||
if getRuntimeFn, err := resolve("PluginGetRuntime"); err == nil {
|
||||
if cs := C.so_call_getruntime(getRuntimeFn); cs != nil {
|
||||
pluginRT = C.GoString(cs)
|
||||
}
|
||||
}
|
||||
|
||||
return &soPlugin{
|
||||
handle: handle,
|
||||
onLoadFn: onLoad,
|
||||
onEventFn: onEvent,
|
||||
onUnloadFn: onUnload,
|
||||
pluginRuntime: pluginRT,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type soPlugin struct {
|
||||
handle unsafe.Pointer // dlopen handle
|
||||
handle unsafe.Pointer
|
||||
onLoadFn unsafe.Pointer
|
||||
onEventFn unsafe.Pointer
|
||||
onUnloadFn unsafe.Pointer
|
||||
cbHandle cgo.Handle
|
||||
pluginRuntime string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
@@ -200,8 +214,14 @@ func (p *soPlugin) Close() error {
|
||||
p.cbHandle = 0
|
||||
}
|
||||
if p.handle != nil {
|
||||
if p.pluginRuntime != "go" {
|
||||
C.so_dlclose(p.handle)
|
||||
}
|
||||
p.handle = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *soPlugin) Runtime() string {
|
||||
return p.pluginRuntime
|
||||
}
|
||||
|
||||
@@ -73,6 +73,11 @@ static void so_call_onunload(void* fn) {
|
||||
typedef void (*fn_t)(void);
|
||||
((fn_t)fn)();
|
||||
}
|
||||
|
||||
static const char* so_call_getruntime(void* fn) {
|
||||
typedef const char* (*fn_t)(void);
|
||||
return ((fn_t)fn)();
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
@@ -145,11 +150,19 @@ func loadNativePlugin(data []byte) (NativePlugin, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pluginRT := "go"
|
||||
if getRuntimeFn, err := resolve("PluginGetRuntime"); err == nil {
|
||||
if cs := C.so_call_getruntime(getRuntimeFn); cs != nil {
|
||||
pluginRT = C.GoString(cs)
|
||||
}
|
||||
}
|
||||
|
||||
return &soPlugin{
|
||||
handle: handle,
|
||||
onLoadFn: onLoad,
|
||||
onEventFn: onEvent,
|
||||
onUnloadFn: onUnload,
|
||||
pluginRuntime: pluginRT,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -159,6 +172,7 @@ type soPlugin struct {
|
||||
onEventFn unsafe.Pointer
|
||||
onUnloadFn unsafe.Pointer
|
||||
cbHandle cgo.Handle
|
||||
pluginRuntime string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
@@ -220,8 +234,14 @@ func (p *soPlugin) Close() error {
|
||||
p.cbHandle = 0
|
||||
}
|
||||
if p.handle != nil {
|
||||
if p.pluginRuntime != "go" {
|
||||
C.so_dlclose(p.handle)
|
||||
}
|
||||
p.handle = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *soPlugin) Runtime() string {
|
||||
return p.pluginRuntime
|
||||
}
|
||||
|
||||
@@ -43,12 +43,29 @@ func loadNativePlugin(data []byte) (NativePlugin, error) {
|
||||
|
||||
setCallback, _ := mm.GetExport("PluginSetCallback")
|
||||
|
||||
runtime := "go"
|
||||
if getRuntimeAddr, err := mm.GetExport("PluginGetRuntime"); err == nil {
|
||||
ret, _, _ := syscall.SyscallN(getRuntimeAddr)
|
||||
if ret != 0 {
|
||||
var buf [32]byte
|
||||
for i := range buf {
|
||||
b := *(*byte)(unsafe.Pointer(ret + uintptr(i)))
|
||||
if b == 0 {
|
||||
runtime = string(buf[:i])
|
||||
break
|
||||
}
|
||||
buf[i] = b
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dp := &dllPlugin{
|
||||
mem: mm,
|
||||
onLoadAddr: onLoad,
|
||||
onEventAddr: onEvent,
|
||||
onUnloadAddr: onUnload,
|
||||
setCallbackAddr: setCallback,
|
||||
pluginRuntime: runtime,
|
||||
}
|
||||
return dp, nil
|
||||
}
|
||||
@@ -60,6 +77,7 @@ type dllPlugin struct {
|
||||
onUnloadAddr uintptr
|
||||
setCallbackAddr uintptr
|
||||
callbackHandle uintptr // prevent GC of the callback closure
|
||||
pluginRuntime string // "go", "c", "cpp", etc.
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
@@ -130,8 +148,17 @@ func (p *dllPlugin) Unload() {
|
||||
|
||||
func (p *dllPlugin) Close() error {
|
||||
p.Unload()
|
||||
// https://github.com/golang/go/issues/11100 as of right now you can't
|
||||
// unload a go c-shared DLL safely
|
||||
if p.pluginRuntime != "go" {
|
||||
if p.mem != nil {
|
||||
p.mem.Free()
|
||||
p.mem = nil
|
||||
}
|
||||
} else {
|
||||
p.mem = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *dllPlugin) Runtime() string {
|
||||
return p.pluginRuntime
|
||||
}
|
||||
|
||||
@@ -184,7 +184,9 @@ func (m *Manager) Load(ctx context.Context, manifest PluginManifest, binary []by
|
||||
m.plugins[pluginID] = pi
|
||||
m.mu.Unlock()
|
||||
|
||||
log.Printf("[plugin] loaded %s (native)", pluginID)
|
||||
rt := np.Runtime()
|
||||
freeable := rt != "go"
|
||||
log.Printf("[plugin] loaded %s (native, runtime=%s, freeable=%v)", pluginID, rt, freeable)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -207,8 +209,14 @@ func (m *Manager) Close() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
for id, pi := range m.plugins {
|
||||
rt := pi.native.Runtime()
|
||||
pi.native.Close()
|
||||
delete(m.plugins, id)
|
||||
if rt != "go" {
|
||||
log.Printf("[plugin] unloaded %s (runtime=%s, memory freed)", id, rt)
|
||||
} else {
|
||||
log.Printf("[plugin] unloaded %s (runtime=go, memory leaked — see golang/go#11100)", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,7 +224,13 @@ func (m *Manager) Unload(pluginId string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if pi, ok := m.plugins[pluginId]; ok {
|
||||
rt := pi.native.Runtime()
|
||||
pi.native.Close()
|
||||
delete(m.plugins, pluginId)
|
||||
if rt != "go" {
|
||||
log.Printf("[plugin] unloaded %s (runtime=%s, memory freed)", pluginId, rt)
|
||||
} else {
|
||||
log.Printf("[plugin] unloaded %s (runtime=go, memory leaked — see golang/go#11100)", pluginId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,8 @@ type NativePlugin interface {
|
||||
Unload()
|
||||
|
||||
Close() error
|
||||
|
||||
Runtime() string
|
||||
}
|
||||
|
||||
func ManifestFromMap(m map[string]interface{}) (PluginManifest, error) {
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
set "ROOT=%~dp0"
|
||||
set "PLUGIN_DIR=%ROOT%plugin-sample-c"
|
||||
if not "%~1"=="" set "PLUGIN_DIR=%~1"
|
||||
|
||||
set "NATIVE_DIR=%PLUGIN_DIR%\native"
|
||||
set "PLUGIN_NAME=sample-c"
|
||||
set "ZIP_OUT=%PLUGIN_DIR%\%PLUGIN_NAME%.zip"
|
||||
|
||||
if not exist "%NATIVE_DIR%\plugin.c" (
|
||||
echo [error] native\plugin.c not found in %NATIVE_DIR%
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Build targets - default to windows-amd64 on Windows
|
||||
if not defined BUILD_TARGETS set "BUILD_TARGETS=windows-amd64"
|
||||
|
||||
set "BUILT_FILES="
|
||||
for %%T in (%BUILD_TARGETS%) do (
|
||||
for /f "tokens=1,2 delims=-" %%A in ("%%T") do (
|
||||
set "TARGET_OS=%%A"
|
||||
set "TARGET_ARCH=%%B"
|
||||
)
|
||||
|
||||
if "!TARGET_OS!"=="windows" (
|
||||
set "EXT=dll"
|
||||
) else if "!TARGET_OS!"=="darwin" (
|
||||
set "EXT=dylib"
|
||||
) else (
|
||||
set "EXT=so"
|
||||
)
|
||||
set "OUTFILE=%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.!EXT!"
|
||||
|
||||
if "!TARGET_OS!"=="windows" (
|
||||
echo [build] cl /LD /O2 "%NATIVE_DIR%\plugin.c" /Fe:"!OUTFILE!"
|
||||
cl /LD /O2 "%NATIVE_DIR%\plugin.c" /Fe:"!OUTFILE!" >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo [build] cl failed, trying gcc...
|
||||
gcc -shared -O2 -o "!OUTFILE!" "%NATIVE_DIR%\plugin.c"
|
||||
if errorlevel 1 (
|
||||
echo [error] build failed for !TARGET_OS!-!TARGET_ARCH!
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
) else (
|
||||
echo [build] gcc -shared -fPIC -O2 -o "!OUTFILE!" "%NATIVE_DIR%\plugin.c"
|
||||
gcc -shared -fPIC -O2 -o "!OUTFILE!" "%NATIVE_DIR%\plugin.c"
|
||||
if errorlevel 1 (
|
||||
echo [error] build failed for !TARGET_OS!-!TARGET_ARCH!
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
set "BUILT_FILES=!BUILT_FILES! '%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.!EXT!'"
|
||||
)
|
||||
|
||||
if exist "%ZIP_OUT%" del /f /q "%ZIP_OUT%"
|
||||
|
||||
REM Collect all build outputs and web assets
|
||||
set "ZIP_SOURCES="
|
||||
for %%T in (%BUILD_TARGETS%) do (
|
||||
for /f "tokens=1,2 delims=-" %%A in ("%%T") do (
|
||||
set "TARGET_OS=%%A"
|
||||
set "TARGET_ARCH=%%B"
|
||||
)
|
||||
if "!TARGET_OS!"=="windows" (
|
||||
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.dll'"
|
||||
) else if "!TARGET_OS!"=="darwin" (
|
||||
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.dylib'"
|
||||
) else (
|
||||
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.so'"
|
||||
)
|
||||
)
|
||||
|
||||
REM Add web assets
|
||||
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.html" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.html'"
|
||||
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.css" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.css'"
|
||||
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.js" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.js'"
|
||||
|
||||
REM Remove leading comma
|
||||
set "ZIP_SOURCES=!ZIP_SOURCES:~1!"
|
||||
|
||||
powershell -NoProfile -Command "Compress-Archive -Path !ZIP_SOURCES! -DestinationPath '%ZIP_OUT%'"
|
||||
if errorlevel 1 (
|
||||
echo [error] zip failed
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [ok] %ZIP_OUT%
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PLUGIN_DIR="${1:-${ROOT_DIR}/plugin-sample-c}"
|
||||
NATIVE_DIR="${PLUGIN_DIR}/native"
|
||||
PLUGIN_NAME="sample-c"
|
||||
ZIP_OUT="${PLUGIN_DIR}/${PLUGIN_NAME}.zip"
|
||||
|
||||
if [[ ! -f "${NATIVE_DIR}/plugin.c" ]]; then
|
||||
echo "[error] native/plugin.c not found in ${NATIVE_DIR}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Detect current OS/arch
|
||||
HOST_OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
HOST_ARCH="$(uname -m)"
|
||||
case "${HOST_ARCH}" in
|
||||
x86_64) HOST_ARCH="amd64" ;;
|
||||
aarch64) HOST_ARCH="arm64" ;;
|
||||
arm64) HOST_ARCH="arm64" ;;
|
||||
esac
|
||||
case "${HOST_OS}" in
|
||||
linux) HOST_OS="linux" ;;
|
||||
darwin) HOST_OS="darwin" ;;
|
||||
esac
|
||||
|
||||
DEFAULT_TARGETS="${HOST_OS}-${HOST_ARCH}"
|
||||
BUILD_TARGETS="${BUILD_TARGETS:-${DEFAULT_TARGETS}}"
|
||||
|
||||
BUILT_FILES=()
|
||||
|
||||
for target in ${BUILD_TARGETS}; do
|
||||
os="${target%%-*}"
|
||||
arch="${target#*-}"
|
||||
|
||||
if [[ "${os}" == "windows" ]]; then
|
||||
ext="dll"
|
||||
elif [[ "${os}" == "darwin" ]]; then
|
||||
ext="dylib"
|
||||
else
|
||||
ext="so"
|
||||
fi
|
||||
|
||||
outfile="${PLUGIN_DIR}/${PLUGIN_NAME}-${os}-${arch}.${ext}"
|
||||
echo "[build] gcc -shared -fPIC -O2 -o ${outfile} ${NATIVE_DIR}/plugin.c"
|
||||
gcc -shared -fPIC -O2 -o "${outfile}" "${NATIVE_DIR}/plugin.c"
|
||||
BUILT_FILES+=("${PLUGIN_NAME}-${os}-${arch}.${ext}")
|
||||
done
|
||||
|
||||
rm -f "${ZIP_OUT}"
|
||||
|
||||
# Collect files to zip
|
||||
ZIP_FILES=()
|
||||
for bf in "${BUILT_FILES[@]}"; do
|
||||
ZIP_FILES+=("${bf}")
|
||||
done
|
||||
for asset in "${PLUGIN_NAME}.html" "${PLUGIN_NAME}.css" "${PLUGIN_NAME}.js"; do
|
||||
if [[ -f "${PLUGIN_DIR}/${asset}" ]]; then
|
||||
ZIP_FILES+=("${asset}")
|
||||
fi
|
||||
done
|
||||
|
||||
if command -v zip >/dev/null 2>&1; then
|
||||
(cd "${PLUGIN_DIR}" && zip -q "${ZIP_OUT}" "${ZIP_FILES[@]}")
|
||||
else
|
||||
echo "[error] zip not found. Please install zip." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[ok] ${ZIP_OUT}"
|
||||
@@ -0,0 +1,90 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
set "ROOT=%~dp0"
|
||||
set "PLUGIN_DIR=%ROOT%plugin-sample-cpp"
|
||||
if not "%~1"=="" set "PLUGIN_DIR=%~1"
|
||||
|
||||
set "NATIVE_DIR=%PLUGIN_DIR%\native"
|
||||
set "PLUGIN_NAME=sample-cpp"
|
||||
set "ZIP_OUT=%PLUGIN_DIR%\%PLUGIN_NAME%.zip"
|
||||
|
||||
if not exist "%NATIVE_DIR%\plugin.cpp" (
|
||||
echo [error] native\plugin.cpp not found in %NATIVE_DIR%
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Build targets - default to windows-amd64 on Windows
|
||||
if not defined BUILD_TARGETS set "BUILD_TARGETS=windows-amd64"
|
||||
|
||||
set "BUILT_FILES="
|
||||
for %%T in (%BUILD_TARGETS%) do (
|
||||
for /f "tokens=1,2 delims=-" %%A in ("%%T") do (
|
||||
set "TARGET_OS=%%A"
|
||||
set "TARGET_ARCH=%%B"
|
||||
)
|
||||
|
||||
if "!TARGET_OS!"=="windows" (
|
||||
set "EXT=dll"
|
||||
) else if "!TARGET_OS!"=="darwin" (
|
||||
set "EXT=dylib"
|
||||
) else (
|
||||
set "EXT=so"
|
||||
)
|
||||
set "OUTFILE=%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.!EXT!"
|
||||
|
||||
if "!TARGET_OS!"=="windows" (
|
||||
echo [build] cl /LD /EHsc /O2 "%NATIVE_DIR%\plugin.cpp" /Fe:"!OUTFILE!"
|
||||
cl /LD /EHsc /O2 "%NATIVE_DIR%\plugin.cpp" /Fe:"!OUTFILE!" >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo [build] cl failed, trying g++...
|
||||
g++ -shared -O2 -o "!OUTFILE!" "%NATIVE_DIR%\plugin.cpp"
|
||||
if errorlevel 1 (
|
||||
echo [error] build failed for !TARGET_OS!-!TARGET_ARCH!
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
) else (
|
||||
echo [build] g++ -shared -fPIC -O2 -o "!OUTFILE!" "%NATIVE_DIR%\plugin.cpp"
|
||||
g++ -shared -fPIC -O2 -o "!OUTFILE!" "%NATIVE_DIR%\plugin.cpp"
|
||||
if errorlevel 1 (
|
||||
echo [error] build failed for !TARGET_OS!-!TARGET_ARCH!
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
set "BUILT_FILES=!BUILT_FILES! '%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.!EXT!'"
|
||||
)
|
||||
|
||||
if exist "%ZIP_OUT%" del /f /q "%ZIP_OUT%"
|
||||
|
||||
REM Collect all build outputs and web assets
|
||||
set "ZIP_SOURCES="
|
||||
for %%T in (%BUILD_TARGETS%) do (
|
||||
for /f "tokens=1,2 delims=-" %%A in ("%%T") do (
|
||||
set "TARGET_OS=%%A"
|
||||
set "TARGET_ARCH=%%B"
|
||||
)
|
||||
if "!TARGET_OS!"=="windows" (
|
||||
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.dll'"
|
||||
) else if "!TARGET_OS!"=="darwin" (
|
||||
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.dylib'"
|
||||
) else (
|
||||
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.so'"
|
||||
)
|
||||
)
|
||||
|
||||
REM Add web assets
|
||||
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.html" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.html'"
|
||||
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.css" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.css'"
|
||||
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.js" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.js'"
|
||||
|
||||
REM Remove leading comma
|
||||
set "ZIP_SOURCES=!ZIP_SOURCES:~1!"
|
||||
|
||||
powershell -NoProfile -Command "Compress-Archive -Path !ZIP_SOURCES! -DestinationPath '%ZIP_OUT%'"
|
||||
if errorlevel 1 (
|
||||
echo [error] zip failed
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [ok] %ZIP_OUT%
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PLUGIN_DIR="${1:-${ROOT_DIR}/plugin-sample-cpp}"
|
||||
NATIVE_DIR="${PLUGIN_DIR}/native"
|
||||
PLUGIN_NAME="sample-cpp"
|
||||
ZIP_OUT="${PLUGIN_DIR}/${PLUGIN_NAME}.zip"
|
||||
|
||||
if [[ ! -f "${NATIVE_DIR}/plugin.cpp" ]]; then
|
||||
echo "[error] native/plugin.cpp not found in ${NATIVE_DIR}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Detect current OS/arch
|
||||
HOST_OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
HOST_ARCH="$(uname -m)"
|
||||
case "${HOST_ARCH}" in
|
||||
x86_64) HOST_ARCH="amd64" ;;
|
||||
aarch64) HOST_ARCH="arm64" ;;
|
||||
arm64) HOST_ARCH="arm64" ;;
|
||||
esac
|
||||
case "${HOST_OS}" in
|
||||
linux) HOST_OS="linux" ;;
|
||||
darwin) HOST_OS="darwin" ;;
|
||||
esac
|
||||
|
||||
DEFAULT_TARGETS="${HOST_OS}-${HOST_ARCH}"
|
||||
BUILD_TARGETS="${BUILD_TARGETS:-${DEFAULT_TARGETS}}"
|
||||
|
||||
BUILT_FILES=()
|
||||
|
||||
for target in ${BUILD_TARGETS}; do
|
||||
os="${target%%-*}"
|
||||
arch="${target#*-}"
|
||||
|
||||
if [[ "${os}" == "windows" ]]; then
|
||||
ext="dll"
|
||||
elif [[ "${os}" == "darwin" ]]; then
|
||||
ext="dylib"
|
||||
else
|
||||
ext="so"
|
||||
fi
|
||||
|
||||
outfile="${PLUGIN_DIR}/${PLUGIN_NAME}-${os}-${arch}.${ext}"
|
||||
echo "[build] g++ -shared -fPIC -O2 -o ${outfile} ${NATIVE_DIR}/plugin.cpp"
|
||||
g++ -shared -fPIC -O2 -o "${outfile}" "${NATIVE_DIR}/plugin.cpp"
|
||||
BUILT_FILES+=("${PLUGIN_NAME}-${os}-${arch}.${ext}")
|
||||
done
|
||||
|
||||
rm -f "${ZIP_OUT}"
|
||||
|
||||
# Collect files to zip
|
||||
ZIP_FILES=()
|
||||
for bf in "${BUILT_FILES[@]}"; do
|
||||
ZIP_FILES+=("${bf}")
|
||||
done
|
||||
for asset in "${PLUGIN_NAME}.html" "${PLUGIN_NAME}.css" "${PLUGIN_NAME}.js"; do
|
||||
if [[ -f "${PLUGIN_DIR}/${asset}" ]]; then
|
||||
ZIP_FILES+=("${asset}")
|
||||
fi
|
||||
done
|
||||
|
||||
if command -v zip >/dev/null 2>&1; then
|
||||
(cd "${PLUGIN_DIR}" && zip -q "${ZIP_OUT}" "${ZIP_FILES[@]}")
|
||||
else
|
||||
echo "[error] zip not found. Please install zip." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[ok] ${ZIP_OUT}"
|
||||
@@ -0,0 +1,100 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
set "ROOT=%~dp0"
|
||||
set "PLUGIN_DIR=%ROOT%plugin-sample-rust"
|
||||
if not "%~1"=="" set "PLUGIN_DIR=%~1"
|
||||
|
||||
set "NATIVE_DIR=%PLUGIN_DIR%\native"
|
||||
set "PLUGIN_NAME=sample-rust"
|
||||
set "ZIP_OUT=%PLUGIN_DIR%\%PLUGIN_NAME%.zip"
|
||||
|
||||
if not exist "%NATIVE_DIR%\Cargo.toml" (
|
||||
echo [error] native\Cargo.toml not found in %NATIVE_DIR%
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Build targets - default to windows-amd64 on Windows
|
||||
if not defined BUILD_TARGETS set "BUILD_TARGETS=windows-amd64"
|
||||
|
||||
set "BUILT_FILES="
|
||||
for %%T in (%BUILD_TARGETS%) do (
|
||||
for /f "tokens=1,2 delims=-" %%A in ("%%T") do (
|
||||
set "TARGET_OS=%%A"
|
||||
set "TARGET_ARCH=%%B"
|
||||
)
|
||||
|
||||
if "!TARGET_OS!"=="windows" (
|
||||
set "EXT=dll"
|
||||
set "RUST_TARGET=x86_64-pc-windows-msvc"
|
||||
if "!TARGET_ARCH!"=="arm64" set "RUST_TARGET=aarch64-pc-windows-msvc"
|
||||
) else if "!TARGET_OS!"=="darwin" (
|
||||
set "EXT=dylib"
|
||||
set "RUST_TARGET=x86_64-apple-darwin"
|
||||
if "!TARGET_ARCH!"=="arm64" set "RUST_TARGET=aarch64-apple-darwin"
|
||||
) else (
|
||||
set "EXT=so"
|
||||
set "RUST_TARGET=x86_64-unknown-linux-gnu"
|
||||
if "!TARGET_ARCH!"=="arm64" set "RUST_TARGET=aarch64-unknown-linux-gnu"
|
||||
)
|
||||
|
||||
set "OUTFILE=%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.!EXT!"
|
||||
|
||||
echo [build] cargo build --release --target=!RUST_TARGET! in %NATIVE_DIR%
|
||||
pushd "%NATIVE_DIR%"
|
||||
cargo build --release --target=!RUST_TARGET!
|
||||
if errorlevel 1 (
|
||||
echo [error] build failed for !TARGET_OS!-!TARGET_ARCH!
|
||||
popd
|
||||
exit /b 1
|
||||
)
|
||||
popd
|
||||
|
||||
REM Cargo outputs to target/<triple>/release/ — find the cdylib
|
||||
if "!TARGET_OS!"=="windows" (
|
||||
copy /Y "%NATIVE_DIR%\target\!RUST_TARGET!\release\sample_rust.dll" "!OUTFILE!" >nul
|
||||
) else if "!TARGET_OS!"=="darwin" (
|
||||
copy /Y "%NATIVE_DIR%\target\!RUST_TARGET!\release\libsample_rust.dylib" "!OUTFILE!" >nul
|
||||
) else (
|
||||
copy /Y "%NATIVE_DIR%\target\!RUST_TARGET!\release\libsample_rust.so" "!OUTFILE!" >nul
|
||||
)
|
||||
if errorlevel 1 (
|
||||
echo [error] copy failed for !TARGET_OS!-!TARGET_ARCH!
|
||||
exit /b 1
|
||||
)
|
||||
set "BUILT_FILES=!BUILT_FILES! '!OUTFILE!'"
|
||||
)
|
||||
|
||||
if exist "%ZIP_OUT%" del /f /q "%ZIP_OUT%"
|
||||
|
||||
REM Collect all build outputs and web assets
|
||||
set "ZIP_SOURCES="
|
||||
for %%T in (%BUILD_TARGETS%) do (
|
||||
for /f "tokens=1,2 delims=-" %%A in ("%%T") do (
|
||||
set "TARGET_OS=%%A"
|
||||
set "TARGET_ARCH=%%B"
|
||||
)
|
||||
if "!TARGET_OS!"=="windows" (
|
||||
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.dll'"
|
||||
) else if "!TARGET_OS!"=="darwin" (
|
||||
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.dylib'"
|
||||
) else (
|
||||
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.so'"
|
||||
)
|
||||
)
|
||||
|
||||
REM Add web assets
|
||||
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.html" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.html'"
|
||||
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.css" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.css'"
|
||||
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.js" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.js'"
|
||||
|
||||
REM Remove leading comma
|
||||
set "ZIP_SOURCES=!ZIP_SOURCES:~1!"
|
||||
|
||||
powershell -NoProfile -Command "Compress-Archive -Path !ZIP_SOURCES! -DestinationPath '%ZIP_OUT%'"
|
||||
if errorlevel 1 (
|
||||
echo [error] zip failed
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [ok] %ZIP_OUT%
|
||||
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PLUGIN_DIR="${1:-${ROOT_DIR}/plugin-sample-rust}"
|
||||
NATIVE_DIR="${PLUGIN_DIR}/native"
|
||||
PLUGIN_NAME="sample-rust"
|
||||
ZIP_OUT="${PLUGIN_DIR}/${PLUGIN_NAME}.zip"
|
||||
|
||||
if [[ ! -f "${NATIVE_DIR}/Cargo.toml" ]]; then
|
||||
echo "[error] native/Cargo.toml not found in ${NATIVE_DIR}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Detect current OS/arch
|
||||
HOST_OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
HOST_ARCH="$(uname -m)"
|
||||
case "${HOST_ARCH}" in
|
||||
x86_64) HOST_ARCH="amd64" ;;
|
||||
aarch64) HOST_ARCH="arm64" ;;
|
||||
arm64) HOST_ARCH="arm64" ;;
|
||||
esac
|
||||
case "${HOST_OS}" in
|
||||
linux) HOST_OS="linux" ;;
|
||||
darwin) HOST_OS="darwin" ;;
|
||||
esac
|
||||
|
||||
DEFAULT_TARGETS="${HOST_OS}-${HOST_ARCH}"
|
||||
BUILD_TARGETS="${BUILD_TARGETS:-${DEFAULT_TARGETS}}"
|
||||
|
||||
BUILT_FILES=()
|
||||
|
||||
for target in ${BUILD_TARGETS}; do
|
||||
os="${target%%-*}"
|
||||
arch="${target#*-}"
|
||||
|
||||
if [[ "${os}" == "windows" ]]; then
|
||||
ext="dll"
|
||||
rust_target="x86_64-pc-windows-gnu"
|
||||
[[ "${arch}" == "arm64" ]] && rust_target="aarch64-pc-windows-gnullvm"
|
||||
lib_name="sample_rust.dll"
|
||||
elif [[ "${os}" == "darwin" ]]; then
|
||||
ext="dylib"
|
||||
rust_target="x86_64-apple-darwin"
|
||||
[[ "${arch}" == "arm64" ]] && rust_target="aarch64-apple-darwin"
|
||||
lib_name="libsample_rust.dylib"
|
||||
else
|
||||
ext="so"
|
||||
rust_target="x86_64-unknown-linux-gnu"
|
||||
[[ "${arch}" == "arm64" ]] && rust_target="aarch64-unknown-linux-gnu"
|
||||
lib_name="libsample_rust.so"
|
||||
fi
|
||||
|
||||
outfile="${PLUGIN_DIR}/${PLUGIN_NAME}-${os}-${arch}.${ext}"
|
||||
echo "[build] cargo build --release --target=${rust_target} in ${NATIVE_DIR}"
|
||||
(cd "${NATIVE_DIR}" && cargo build --release --target="${rust_target}")
|
||||
cp "${NATIVE_DIR}/target/${rust_target}/release/${lib_name}" "${outfile}"
|
||||
BUILT_FILES+=("${PLUGIN_NAME}-${os}-${arch}.${ext}")
|
||||
done
|
||||
|
||||
rm -f "${ZIP_OUT}"
|
||||
|
||||
# Collect files to zip
|
||||
ZIP_FILES=()
|
||||
for bf in "${BUILT_FILES[@]}"; do
|
||||
ZIP_FILES+=("${bf}")
|
||||
done
|
||||
for asset in "${PLUGIN_NAME}.html" "${PLUGIN_NAME}.css" "${PLUGIN_NAME}.js"; do
|
||||
if [[ -f "${PLUGIN_DIR}/${asset}" ]]; then
|
||||
ZIP_FILES+=("${asset}")
|
||||
fi
|
||||
done
|
||||
|
||||
if command -v zip >/dev/null 2>&1; then
|
||||
(cd "${PLUGIN_DIR}" && zip -q "${ZIP_OUT}" "${ZIP_FILES[@]}")
|
||||
else
|
||||
echo "[error] zip not found. Please install zip." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[ok] ${ZIP_OUT}"
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Sample C Plugin — fully unloadable, no runtime baggage.
|
||||
*
|
||||
* Exports the standard Overlord plugin ABI:
|
||||
* PluginOnLoad, PluginOnEvent, PluginOnUnload, PluginSetCallback,
|
||||
* PluginGetRuntime (returns "c" — tells the host this DLL can be freed)
|
||||
*
|
||||
* Build (Windows):
|
||||
* cl /LD /O2 plugin.c /Fe:sample-c-windows-amd64.dll
|
||||
* — or with MinGW —
|
||||
* x86_64-w64-mingw32-gcc -shared -O2 -o sample-c-windows-amd64.dll plugin.c
|
||||
*
|
||||
* Build (Linux):
|
||||
* gcc -shared -fPIC -O2 -o sample-c-linux-amd64.so plugin.c
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Platform helpers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
#ifdef _WIN32
|
||||
# define EXPORT __declspec(dllexport)
|
||||
# define WIN32_LEAN_AND_MEAN
|
||||
# include <windows.h>
|
||||
BOOL WINAPI DllMain(HINSTANCE hDll, DWORD reason, LPVOID reserved) {
|
||||
(void)hDll; (void)reserved;
|
||||
switch (reason) {
|
||||
case DLL_PROCESS_ATTACH: break;
|
||||
case DLL_PROCESS_DETACH: break;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
#else
|
||||
# define EXPORT __attribute__((visibility("default")))
|
||||
#endif
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Host callback */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
#ifdef _WIN32
|
||||
/* Windows: callback is void(__stdcall*)(ptr,len,ptr,len) */
|
||||
typedef void (__stdcall *host_callback_t)(
|
||||
const char *event, uintptr_t eventLen,
|
||||
const char *payload, uintptr_t payloadLen);
|
||||
#else
|
||||
/* Unix: callback is void(*)(ctx, event, eventLen, payload, payloadLen) */
|
||||
typedef void (*host_callback_t)(
|
||||
uintptr_t ctx,
|
||||
const char *event, int eventLen,
|
||||
const char *payload, int payloadLen);
|
||||
#endif
|
||||
|
||||
static host_callback_t g_callback = NULL;
|
||||
#ifndef _WIN32
|
||||
static uintptr_t g_callback_ctx = 0;
|
||||
#endif
|
||||
|
||||
static void send_event(const char *event, const char *payload) {
|
||||
if (!g_callback) return;
|
||||
int elen = event ? (int)strlen(event) : 0;
|
||||
int plen = payload ? (int)strlen(payload) : 0;
|
||||
#ifdef _WIN32
|
||||
g_callback(event, (uintptr_t)elen, payload, (uintptr_t)plen);
|
||||
#else
|
||||
g_callback(g_callback_ctx, event, elen, payload, plen);
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Plugin state (trivial example) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static char g_client_id[256];
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Exported ABI */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
EXPORT const char *PluginGetRuntime(void) {
|
||||
return "c";
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
EXPORT void PluginSetCallback(uint64_t cb) {
|
||||
g_callback = (host_callback_t)(uintptr_t)cb;
|
||||
}
|
||||
|
||||
EXPORT int PluginOnLoad(const char *hostInfo, int hostInfoLen, uint64_t cb) {
|
||||
g_callback = (host_callback_t)(uintptr_t)cb;
|
||||
#else
|
||||
EXPORT int PluginOnLoad(const char *hostInfo, int hostInfoLen,
|
||||
uintptr_t cb, uintptr_t ctx) {
|
||||
g_callback = (host_callback_t)cb;
|
||||
g_callback_ctx = ctx;
|
||||
#endif
|
||||
/* Minimal JSON-ish parse to grab clientId — keeps the example dependency-free */
|
||||
g_client_id[0] = '\0';
|
||||
if (hostInfo && hostInfoLen > 0) {
|
||||
const char *key = "\"clientId\":\"";
|
||||
const char *p = strstr(hostInfo, key); /* safe: hostInfo is nul-terminated by the Go caller copying it */
|
||||
if (!p) {
|
||||
key = "\"clientId\": \"";
|
||||
p = strstr(hostInfo, key);
|
||||
}
|
||||
if (p) {
|
||||
p += strlen(key);
|
||||
const char *end = strchr(p, '"');
|
||||
if (end) {
|
||||
size_t n = (size_t)(end - p);
|
||||
if (n >= sizeof(g_client_id)) n = sizeof(g_client_id) - 1;
|
||||
memcpy(g_client_id, p, n);
|
||||
g_client_id[n] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stderr, "[sample-c] loaded, clientId=%s\n", g_client_id);
|
||||
send_event("ready", "{\"message\":\"sample-c plugin ready\"}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
EXPORT int PluginOnEvent(const char *event, int eventLen,
|
||||
const char *payload, int payloadLen) {
|
||||
(void)payloadLen;
|
||||
|
||||
/* Simple echo for "ping" */
|
||||
if (eventLen == 4 && memcmp(event, "ping", 4) == 0) {
|
||||
send_event("pong", NULL);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Echo back ui_message */
|
||||
if (eventLen == 10 && memcmp(event, "ui_message", 10) == 0) {
|
||||
fprintf(stderr, "[sample-c] got ui_message: %.*s\n", payloadLen, payload ? payload : "");
|
||||
/* Build a tiny JSON response — payload is already JSON from the host */
|
||||
char buf[512];
|
||||
snprintf(buf, sizeof(buf), "{\"message\":\"echo from C: %.*s\"}",
|
||||
payloadLen < 400 ? payloadLen : 400, payload ? payload : "");
|
||||
send_event("echo", buf);
|
||||
return 0;
|
||||
}
|
||||
|
||||
fprintf(stderr, "[sample-c] unhandled event: %.*s\n", eventLen, event);
|
||||
return 0;
|
||||
}
|
||||
|
||||
EXPORT void PluginOnUnload(void) {
|
||||
fprintf(stderr, "[sample-c] unloading\n");
|
||||
g_callback = NULL;
|
||||
#ifndef _WIN32
|
||||
g_callback_ctx = 0;
|
||||
#endif
|
||||
g_client_id[0] = '\0';
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Segoe UI", system-ui, sans-serif;
|
||||
background: #0f1115;
|
||||
color: #e8ecf1;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 820px;
|
||||
margin: 40px auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.sub {
|
||||
margin: 0 0 24px 0;
|
||||
color: #9aa4b2;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #161a22;
|
||||
border: 1px solid #222938;
|
||||
border-radius: 12px;
|
||||
padding: 18px;
|
||||
margin-bottom: 18px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #9aa4b2;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #2a3346;
|
||||
background: #0f131b;
|
||||
color: #e8ecf1;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #1d4ed8;
|
||||
}
|
||||
|
||||
.log {
|
||||
background: #0b0e14;
|
||||
border: 1px solid #1e2533;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Sample C Plugin</title>
|
||||
<link rel="stylesheet" href="/plugins/sample-c/assets/sample-c.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="container">
|
||||
<header class="header">
|
||||
<h1>Sample C Plugin</h1>
|
||||
<p class="sub">Pure C — fully unloadable, zero runtime leak</p>
|
||||
</header>
|
||||
|
||||
<section class="card">
|
||||
<div class="row">
|
||||
<label class="label">Client ID</label>
|
||||
<input id="client-id" class="input" placeholder="clientId" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label class="label">Message</label>
|
||||
<input id="message" class="input" placeholder="hello from UI" />
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button id="send-btn" class="btn">Send event to client</button>
|
||||
<button id="ping-btn" class="btn">Ping</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Log</h2>
|
||||
<pre id="log" class="log"></pre>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/plugins/sample-c/assets/sample-c.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,43 @@
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const clientIdInput = document.getElementById("client-id");
|
||||
const messageInput = document.getElementById("message");
|
||||
const logEl = document.getElementById("log");
|
||||
const sendBtn = document.getElementById("send-btn");
|
||||
const pingBtn = document.getElementById("ping-btn");
|
||||
|
||||
const pluginId = "sample-c";
|
||||
clientIdInput.value = params.get("clientId") || "";
|
||||
messageInput.value = "hello from sample-c UI";
|
||||
|
||||
function log(line) {
|
||||
const ts = new Date().toISOString();
|
||||
logEl.textContent = `${ts} ${line}\n` + logEl.textContent;
|
||||
}
|
||||
|
||||
async function sendEvent(event, payload) {
|
||||
const clientId = clientIdInput.value.trim();
|
||||
if (!clientId) { log("Missing clientId"); return; }
|
||||
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/plugins/${pluginId}/event`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ event, payload }),
|
||||
});
|
||||
if (!res.ok) { log(`Send failed: ${res.status} ${await res.text()}`); return; }
|
||||
log(`Sent event: ${event}`);
|
||||
}
|
||||
|
||||
sendBtn.addEventListener("click", () => {
|
||||
sendEvent("ui_message", { message: messageInput.value.trim() });
|
||||
});
|
||||
|
||||
pingBtn.addEventListener("click", () => {
|
||||
sendEvent("ping", {});
|
||||
});
|
||||
|
||||
// Listen for plugin events via SSE / polling (same pattern as Go sample)
|
||||
if (typeof EventSource !== "undefined") {
|
||||
const clientId = params.get("clientId");
|
||||
if (clientId) {
|
||||
log("Ready — C plugin (fully unloadable)");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
* Sample C++ Plugin — fully unloadable, no runtime baggage.
|
||||
*
|
||||
* Exports the standard Overlord plugin ABI:
|
||||
* PluginOnLoad, PluginOnEvent, PluginOnUnload, PluginSetCallback,
|
||||
* PluginGetRuntime (returns "cpp" — tells the host this DLL can be freed)
|
||||
*
|
||||
* Build (Windows / MSVC):
|
||||
* cl /LD /EHsc /O2 plugin.cpp /Fe:sample-cpp-windows-amd64.dll
|
||||
* — or with MinGW —
|
||||
* x86_64-w64-mingw32-g++ -shared -O2 -o sample-cpp-windows-amd64.dll plugin.cpp
|
||||
*
|
||||
* Build (Linux):
|
||||
* g++ -shared -fPIC -O2 -o sample-cpp-linux-amd64.so plugin.cpp
|
||||
*/
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Platform helpers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
#ifdef _WIN32
|
||||
# define EXPORT extern "C" __declspec(dllexport)
|
||||
# define WIN32_LEAN_AND_MEAN
|
||||
# include <windows.h>
|
||||
BOOL WINAPI DllMain(HINSTANCE hDll, DWORD reason, LPVOID reserved) {
|
||||
(void)hDll; (void)reserved;
|
||||
return TRUE;
|
||||
}
|
||||
#else
|
||||
# define EXPORT extern "C" __attribute__((visibility("default")))
|
||||
#endif
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Host callback */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
#ifdef _WIN32
|
||||
typedef void (__stdcall *host_callback_t)(
|
||||
const char *event, uintptr_t eventLen,
|
||||
const char *payload, uintptr_t payloadLen);
|
||||
#else
|
||||
typedef void (*host_callback_t)(
|
||||
uintptr_t ctx,
|
||||
const char *event, int eventLen,
|
||||
const char *payload, int payloadLen);
|
||||
#endif
|
||||
|
||||
static host_callback_t g_callback = nullptr;
|
||||
#ifndef _WIN32
|
||||
static uintptr_t g_callback_ctx = 0;
|
||||
#endif
|
||||
|
||||
static void send_event(const char *event, const char *payload) {
|
||||
if (!g_callback) return;
|
||||
int elen = event ? static_cast<int>(strlen(event)) : 0;
|
||||
int plen = payload ? static_cast<int>(strlen(payload)) : 0;
|
||||
#ifdef _WIN32
|
||||
g_callback(event, static_cast<uintptr_t>(elen),
|
||||
payload, static_cast<uintptr_t>(plen));
|
||||
#else
|
||||
g_callback(g_callback_ctx, event, elen, payload, plen);
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Plugin state — demonstrates C++ features (std::string, maps, mutex) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static std::mutex g_mu;
|
||||
static std::string g_client_id;
|
||||
static std::unordered_map<std::string, int> g_event_counts;
|
||||
|
||||
/* Extract a JSON string value by key (simple, no full JSON parser) */
|
||||
static std::string json_extract(const char *json, int len, const char *key) {
|
||||
if (!json || len <= 0) return "";
|
||||
std::string haystack(json, static_cast<size_t>(len));
|
||||
std::string needle = std::string("\"") + key + "\":\"";
|
||||
auto pos = haystack.find(needle);
|
||||
if (pos == std::string::npos) {
|
||||
needle = std::string("\"") + key + "\": \"";
|
||||
pos = haystack.find(needle);
|
||||
}
|
||||
if (pos == std::string::npos) return "";
|
||||
pos += needle.size();
|
||||
auto end = haystack.find('"', pos);
|
||||
if (end == std::string::npos) return "";
|
||||
return haystack.substr(pos, end - pos);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Exported ABI */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
EXPORT const char *PluginGetRuntime() {
|
||||
return "cpp";
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
EXPORT void PluginSetCallback(uint64_t cb) {
|
||||
g_callback = reinterpret_cast<host_callback_t>(static_cast<uintptr_t>(cb));
|
||||
}
|
||||
|
||||
EXPORT int PluginOnLoad(const char *hostInfo, int hostInfoLen, uint64_t cb) {
|
||||
g_callback = reinterpret_cast<host_callback_t>(static_cast<uintptr_t>(cb));
|
||||
#else
|
||||
EXPORT int PluginOnLoad(const char *hostInfo, int hostInfoLen,
|
||||
uintptr_t cb, uintptr_t ctx) {
|
||||
g_callback = reinterpret_cast<host_callback_t>(cb);
|
||||
g_callback_ctx = ctx;
|
||||
#endif
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_mu);
|
||||
g_client_id = json_extract(hostInfo, hostInfoLen, "clientId");
|
||||
g_event_counts.clear();
|
||||
}
|
||||
|
||||
fprintf(stderr, "[sample-cpp] loaded, clientId=%s\n", g_client_id.c_str());
|
||||
send_event("ready", "{\"message\":\"sample-cpp plugin ready\"}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
EXPORT int PluginOnEvent(const char *event, int eventLen,
|
||||
const char *payload, int payloadLen) {
|
||||
std::string ev(event, static_cast<size_t>(eventLen));
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_mu);
|
||||
g_event_counts[ev]++;
|
||||
}
|
||||
|
||||
if (ev == "ping") {
|
||||
send_event("pong", nullptr);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (ev == "ui_message") {
|
||||
std::string pl(payload ? payload : "", payload ? static_cast<size_t>(payloadLen) : 0u);
|
||||
fprintf(stderr, "[sample-cpp] got ui_message: %s\n", pl.c_str());
|
||||
std::string resp = "{\"message\":\"echo from C++: " + pl + "\"}";
|
||||
send_event("echo", resp.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (ev == "stats") {
|
||||
std::lock_guard<std::mutex> lk(g_mu);
|
||||
std::string json = "{";
|
||||
bool first = true;
|
||||
for (auto &kv : g_event_counts) {
|
||||
if (!first) json += ",";
|
||||
json += "\"" + kv.first + "\":" + std::to_string(kv.second);
|
||||
first = false;
|
||||
}
|
||||
json += "}";
|
||||
send_event("stats_reply", json.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
fprintf(stderr, "[sample-cpp] unhandled event: %s\n", ev.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
EXPORT void PluginOnUnload() {
|
||||
fprintf(stderr, "[sample-cpp] unloading\n");
|
||||
g_callback = nullptr;
|
||||
#ifndef _WIN32
|
||||
g_callback_ctx = 0;
|
||||
#endif
|
||||
std::lock_guard<std::mutex> lk(g_mu);
|
||||
g_client_id.clear();
|
||||
g_event_counts.clear();
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Segoe UI", system-ui, sans-serif;
|
||||
background: #0f1115;
|
||||
color: #e8ecf1;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 820px;
|
||||
margin: 40px auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.sub {
|
||||
margin: 0 0 24px 0;
|
||||
color: #9aa4b2;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #161a22;
|
||||
border: 1px solid #222938;
|
||||
border-radius: 12px;
|
||||
padding: 18px;
|
||||
margin-bottom: 18px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #9aa4b2;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #2a3346;
|
||||
background: #0f131b;
|
||||
color: #e8ecf1;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #1d4ed8;
|
||||
}
|
||||
|
||||
.log {
|
||||
background: #0b0e14;
|
||||
border: 1px solid #1e2533;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Sample C++ Plugin</title>
|
||||
<link rel="stylesheet" href="/plugins/sample-cpp/assets/sample-cpp.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="container">
|
||||
<header class="header">
|
||||
<h1>Sample C++ Plugin</h1>
|
||||
<p class="sub">C++ — fully unloadable, zero runtime leak</p>
|
||||
</header>
|
||||
|
||||
<section class="card">
|
||||
<div class="row">
|
||||
<label class="label">Client ID</label>
|
||||
<input id="client-id" class="input" placeholder="clientId" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label class="label">Message</label>
|
||||
<input id="message" class="input" placeholder="hello from UI" />
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button id="send-btn" class="btn">Send event to client</button>
|
||||
<button id="ping-btn" class="btn">Ping</button>
|
||||
<button id="stats-btn" class="btn">Get Stats</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Log</h2>
|
||||
<pre id="log" class="log"></pre>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/plugins/sample-cpp/assets/sample-cpp.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,47 @@
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const clientIdInput = document.getElementById("client-id");
|
||||
const messageInput = document.getElementById("message");
|
||||
const logEl = document.getElementById("log");
|
||||
const sendBtn = document.getElementById("send-btn");
|
||||
const pingBtn = document.getElementById("ping-btn");
|
||||
const statsBtn = document.getElementById("stats-btn");
|
||||
|
||||
const pluginId = "sample-cpp";
|
||||
clientIdInput.value = params.get("clientId") || "";
|
||||
messageInput.value = "hello from sample-cpp UI";
|
||||
|
||||
function log(line) {
|
||||
const ts = new Date().toISOString();
|
||||
logEl.textContent = `${ts} ${line}\n` + logEl.textContent;
|
||||
}
|
||||
|
||||
async function sendEvent(event, payload) {
|
||||
const clientId = clientIdInput.value.trim();
|
||||
if (!clientId) { log("Missing clientId"); return; }
|
||||
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/plugins/${pluginId}/event`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ event, payload }),
|
||||
});
|
||||
if (!res.ok) { log(`Send failed: ${res.status} ${await res.text()}`); return; }
|
||||
log(`Sent event: ${event}`);
|
||||
}
|
||||
|
||||
sendBtn.addEventListener("click", () => {
|
||||
sendEvent("ui_message", { message: messageInput.value.trim() });
|
||||
});
|
||||
|
||||
pingBtn.addEventListener("click", () => {
|
||||
sendEvent("ping", {});
|
||||
});
|
||||
|
||||
statsBtn.addEventListener("click", () => {
|
||||
sendEvent("stats", {});
|
||||
});
|
||||
|
||||
if (typeof EventSource !== "undefined") {
|
||||
const clientId = params.get("clientId");
|
||||
if (clientId) {
|
||||
log("Ready — C++ plugin (fully unloadable)");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "sample-rust"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
strip = true
|
||||
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
* Sample Rust Plugin — fully unloadable, no GC runtime.
|
||||
*
|
||||
* Exports the standard Overlord plugin ABI:
|
||||
* PluginOnLoad, PluginOnEvent, PluginOnUnload, PluginSetCallback,
|
||||
* PluginGetRuntime (returns "rust" — tells the host this DLL can be freed)
|
||||
*
|
||||
* Build:
|
||||
* cargo build --release
|
||||
* Output: target/release/sample_rust.dll (Windows) or libsample_rust.so (Linux)
|
||||
*
|
||||
* Note: Uses C-style globals instead of std::sync::Mutex because the DLL is
|
||||
* loaded via an in-memory PE loader, and Rust's std Mutex (SRWLOCK) may not
|
||||
* initialize correctly without OS-level LoadLibrary. The Go host already
|
||||
* serializes all calls through its own mutex, so this is safe.
|
||||
*/
|
||||
|
||||
use std::os::raw::{c_char, c_int};
|
||||
use std::slice;
|
||||
use std::ptr;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Host callback types — platform-specific calling conventions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(windows)]
|
||||
type HostCallback = unsafe extern "stdcall" fn(
|
||||
event: *const u8,
|
||||
event_len: usize,
|
||||
payload: *const u8,
|
||||
payload_len: usize,
|
||||
);
|
||||
|
||||
#[cfg(not(windows))]
|
||||
type HostCallback = unsafe extern "C" fn(
|
||||
ctx: usize,
|
||||
event: *const u8,
|
||||
event_len: i32,
|
||||
payload: *const u8,
|
||||
payload_len: i32,
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Global state — C-style, no Mutex.
|
||||
// The Go host serializes all calls via dllPlugin.mu / soPlugin.mu.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static mut G_CALLBACK: Option<HostCallback> = None;
|
||||
#[cfg(not(windows))]
|
||||
static mut G_CALLBACK_CTX: usize = 0;
|
||||
static mut G_CLIENT_ID: [u8; 256] = [0u8; 256];
|
||||
static mut G_CLIENT_ID_LEN: usize = 0;
|
||||
static mut G_EVENT_COUNT: u64 = 0;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
unsafe fn send_event(event: &[u8], payload: &[u8]) {
|
||||
if let Some(cb) = G_CALLBACK {
|
||||
#[cfg(windows)]
|
||||
cb(
|
||||
event.as_ptr(),
|
||||
event.len(),
|
||||
payload.as_ptr(),
|
||||
payload.len(),
|
||||
);
|
||||
#[cfg(not(windows))]
|
||||
cb(
|
||||
G_CALLBACK_CTX,
|
||||
event.as_ptr(),
|
||||
event.len() as i32,
|
||||
payload.as_ptr(),
|
||||
payload.len() as i32,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimal JSON string extraction by key — zero allocations.
|
||||
unsafe fn json_extract_into(json: &[u8], key: &[u8], out: &mut [u8]) -> usize {
|
||||
// Search for "key":" or "key": "
|
||||
let klen = key.len();
|
||||
if json.len() < klen + 4 {
|
||||
return 0;
|
||||
}
|
||||
let mut i = 0;
|
||||
while i + klen + 3 < json.len() {
|
||||
if json[i] == b'"'
|
||||
&& json[i + 1..i + 1 + klen] == *key
|
||||
&& json[i + 1 + klen] == b'"'
|
||||
{
|
||||
// Skip past "key"
|
||||
let mut j = i + 1 + klen + 1; // past closing quote
|
||||
// Skip : and optional space
|
||||
while j < json.len() && (json[j] == b':' || json[j] == b' ') {
|
||||
j += 1;
|
||||
}
|
||||
// Expect opening quote
|
||||
if j < json.len() && json[j] == b'"' {
|
||||
j += 1;
|
||||
let start = j;
|
||||
while j < json.len() && json[j] != b'"' {
|
||||
j += 1;
|
||||
}
|
||||
let vlen = j - start;
|
||||
let copy_len = if vlen < out.len() { vlen } else { out.len() - 1 };
|
||||
ptr::copy_nonoverlapping(json[start..].as_ptr(), out.as_mut_ptr(), copy_len);
|
||||
return copy_len;
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
unsafe fn bytes_to_slice<'a>(p: *const c_char, len: c_int) -> &'a [u8] {
|
||||
if p.is_null() || len <= 0 {
|
||||
return &[];
|
||||
}
|
||||
slice::from_raw_parts(p as *const u8, len as usize)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Exported ABI
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn PluginGetRuntime() -> *const c_char {
|
||||
b"rust\0".as_ptr() as *const c_char
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn PluginSetCallback(cb: u64) {
|
||||
G_CALLBACK = Some(std::mem::transmute(cb as usize));
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn PluginOnLoad(
|
||||
host_info: *const c_char,
|
||||
host_info_len: c_int,
|
||||
cb: u64,
|
||||
) -> c_int {
|
||||
G_CALLBACK = Some(std::mem::transmute(cb as usize));
|
||||
G_EVENT_COUNT = 0;
|
||||
|
||||
let info = bytes_to_slice(host_info, host_info_len);
|
||||
G_CLIENT_ID_LEN = json_extract_into(info, b"clientId", &mut G_CLIENT_ID);
|
||||
|
||||
// Build a small on-stack response and send
|
||||
send_event(
|
||||
b"ready",
|
||||
br#"{"message":"sample-rust plugin ready"}"#,
|
||||
);
|
||||
0
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn PluginOnLoad(
|
||||
host_info: *const c_char,
|
||||
host_info_len: c_int,
|
||||
cb: usize,
|
||||
ctx: usize,
|
||||
) -> c_int {
|
||||
G_CALLBACK = Some(std::mem::transmute(cb));
|
||||
G_CALLBACK_CTX = ctx;
|
||||
G_EVENT_COUNT = 0;
|
||||
|
||||
let info = bytes_to_slice(host_info, host_info_len);
|
||||
G_CLIENT_ID_LEN = json_extract_into(info, b"clientId", &mut G_CLIENT_ID);
|
||||
|
||||
send_event(
|
||||
b"ready",
|
||||
br#"{"message":"sample-rust plugin ready"}"#,
|
||||
);
|
||||
0
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn PluginOnEvent(
|
||||
event: *const c_char,
|
||||
event_len: c_int,
|
||||
payload: *const c_char,
|
||||
payload_len: c_int,
|
||||
) -> c_int {
|
||||
let ev = bytes_to_slice(event, event_len);
|
||||
let pl = bytes_to_slice(payload, payload_len);
|
||||
|
||||
G_EVENT_COUNT += 1;
|
||||
|
||||
if ev == b"ping" {
|
||||
send_event(b"pong", &[]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ev == b"ui_message" {
|
||||
// Build echo response on the stack — limited to 512 bytes
|
||||
let prefix = br#"{"message":"echo from Rust: "#;
|
||||
let suffix = br#""}"#;
|
||||
let max_pl = 450;
|
||||
let pl_copy = if pl.len() < max_pl { pl.len() } else { max_pl };
|
||||
let total = prefix.len() + pl_copy + suffix.len();
|
||||
let mut buf = [0u8; 512];
|
||||
if total <= buf.len() {
|
||||
ptr::copy_nonoverlapping(prefix.as_ptr(), buf.as_mut_ptr(), prefix.len());
|
||||
ptr::copy_nonoverlapping(
|
||||
pl.as_ptr(),
|
||||
buf.as_mut_ptr().add(prefix.len()),
|
||||
pl_copy,
|
||||
);
|
||||
ptr::copy_nonoverlapping(
|
||||
suffix.as_ptr(),
|
||||
buf.as_mut_ptr().add(prefix.len() + pl_copy),
|
||||
suffix.len(),
|
||||
);
|
||||
send_event(b"echo", &buf[..total]);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ev == b"stats" {
|
||||
// Format event count into a stack buffer
|
||||
let mut buf = [0u8; 128];
|
||||
let prefix = br#"{"event_count":"#;
|
||||
let suffix = b"}";
|
||||
ptr::copy_nonoverlapping(prefix.as_ptr(), buf.as_mut_ptr(), prefix.len());
|
||||
let mut num_buf = [0u8; 20];
|
||||
let num_len = fmt_u64(G_EVENT_COUNT, &mut num_buf);
|
||||
ptr::copy_nonoverlapping(
|
||||
num_buf[20 - num_len..].as_ptr(),
|
||||
buf.as_mut_ptr().add(prefix.len()),
|
||||
num_len,
|
||||
);
|
||||
ptr::copy_nonoverlapping(
|
||||
suffix.as_ptr(),
|
||||
buf.as_mut_ptr().add(prefix.len() + num_len),
|
||||
suffix.len(),
|
||||
);
|
||||
let total = prefix.len() + num_len + suffix.len();
|
||||
send_event(b"stats_reply", &buf[..total]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn PluginOnUnload() {
|
||||
G_CALLBACK = None;
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
G_CALLBACK_CTX = 0;
|
||||
}
|
||||
G_CLIENT_ID_LEN = 0;
|
||||
G_EVENT_COUNT = 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tiny u64-to-decimal without std::fmt (avoids allocator)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn fmt_u64(mut n: u64, buf: &mut [u8; 20]) -> usize {
|
||||
if n == 0 {
|
||||
buf[19] = b'0';
|
||||
return 1;
|
||||
}
|
||||
let mut i = 20;
|
||||
while n > 0 {
|
||||
i -= 1;
|
||||
buf[i] = b'0' + (n % 10) as u8;
|
||||
n /= 10;
|
||||
}
|
||||
20 - i
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Segoe UI", system-ui, sans-serif;
|
||||
background: #0f1115;
|
||||
color: #e8ecf1;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 820px;
|
||||
margin: 40px auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.sub {
|
||||
margin: 0 0 24px 0;
|
||||
color: #9aa4b2;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #161a22;
|
||||
border: 1px solid #222938;
|
||||
border-radius: 12px;
|
||||
padding: 18px;
|
||||
margin-bottom: 18px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #9aa4b2;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #2a3346;
|
||||
background: #0f131b;
|
||||
color: #e8ecf1;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #1d4ed8;
|
||||
}
|
||||
|
||||
.log {
|
||||
background: #0b0e14;
|
||||
border: 1px solid #1e2533;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Sample Rust Plugin</title>
|
||||
<link rel="stylesheet" href="/plugins/sample-rust/assets/sample-rust.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="container">
|
||||
<header class="header">
|
||||
<h1>Sample Rust Plugin</h1>
|
||||
<p class="sub">Rust — fully unloadable, zero runtime leak</p>
|
||||
</header>
|
||||
|
||||
<section class="card">
|
||||
<div class="row">
|
||||
<label class="label">Client ID</label>
|
||||
<input id="client-id" class="input" placeholder="clientId" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label class="label">Message</label>
|
||||
<input id="message" class="input" placeholder="hello from UI" />
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button id="send-btn" class="btn">Send event to client</button>
|
||||
<button id="ping-btn" class="btn">Ping</button>
|
||||
<button id="stats-btn" class="btn">Get Stats</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Log</h2>
|
||||
<pre id="log" class="log"></pre>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/plugins/sample-rust/assets/sample-rust.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,47 @@
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const clientIdInput = document.getElementById("client-id");
|
||||
const messageInput = document.getElementById("message");
|
||||
const logEl = document.getElementById("log");
|
||||
const sendBtn = document.getElementById("send-btn");
|
||||
const pingBtn = document.getElementById("ping-btn");
|
||||
const statsBtn = document.getElementById("stats-btn");
|
||||
|
||||
const pluginId = "sample-rust";
|
||||
clientIdInput.value = params.get("clientId") || "";
|
||||
messageInput.value = "hello from sample-rust UI";
|
||||
|
||||
function log(line) {
|
||||
const ts = new Date().toISOString();
|
||||
logEl.textContent = `${ts} ${line}\n` + logEl.textContent;
|
||||
}
|
||||
|
||||
async function sendEvent(event, payload) {
|
||||
const clientId = clientIdInput.value.trim();
|
||||
if (!clientId) { log("Missing clientId"); return; }
|
||||
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/plugins/${pluginId}/event`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ event, payload }),
|
||||
});
|
||||
if (!res.ok) { log(`Send failed: ${res.status} ${await res.text()}`); return; }
|
||||
log(`Sent event: ${event}`);
|
||||
}
|
||||
|
||||
sendBtn.addEventListener("click", () => {
|
||||
sendEvent("ui_message", { message: messageInput.value.trim() });
|
||||
});
|
||||
|
||||
pingBtn.addEventListener("click", () => {
|
||||
sendEvent("ping", {});
|
||||
});
|
||||
|
||||
statsBtn.addEventListener("click", () => {
|
||||
sendEvent("stats", {});
|
||||
});
|
||||
|
||||
if (typeof EventSource !== "undefined") {
|
||||
const clientId = params.get("clientId");
|
||||
if (clientId) {
|
||||
log("Ready — Rust plugin (fully unloadable)");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user