diff --git a/Overlord-Client/cmd/agent/plugins/loader_darwin.go b/Overlord-Client/cmd/agent/plugins/loader_darwin.go index 31f370f..d9b4dba 100644 --- a/Overlord-Client/cmd/agent/plugins/loader_darwin.go +++ b/Overlord-Client/cmd/agent/plugins/loader_darwin.go @@ -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,21 +130,30 @@ 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, + handle: handle, + onLoadFn: onLoad, + onEventFn: onEvent, + onUnloadFn: onUnload, + pluginRuntime: pluginRT, }, nil } type soPlugin struct { - handle unsafe.Pointer // dlopen handle - onLoadFn unsafe.Pointer - onEventFn unsafe.Pointer - onUnloadFn unsafe.Pointer - cbHandle cgo.Handle - mu sync.Mutex + handle unsafe.Pointer + onLoadFn unsafe.Pointer + onEventFn unsafe.Pointer + onUnloadFn unsafe.Pointer + cbHandle cgo.Handle + pluginRuntime string + mu sync.Mutex } func (p *soPlugin) Load(send func(string, []byte), hostInfo []byte) error { @@ -200,8 +214,14 @@ func (p *soPlugin) Close() error { p.cbHandle = 0 } if p.handle != nil { - C.so_dlclose(p.handle) + if p.pluginRuntime != "go" { + C.so_dlclose(p.handle) + } p.handle = nil } return nil } + +func (p *soPlugin) Runtime() string { + return p.pluginRuntime +} diff --git a/Overlord-Client/cmd/agent/plugins/loader_linux.go b/Overlord-Client/cmd/agent/plugins/loader_linux.go index c236bc1..0be1426 100644 --- a/Overlord-Client/cmd/agent/plugins/loader_linux.go +++ b/Overlord-Client/cmd/agent/plugins/loader_linux.go @@ -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,21 +150,30 @@ 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, + handle: handle, + onLoadFn: onLoad, + onEventFn: onEvent, + onUnloadFn: onUnload, + pluginRuntime: pluginRT, }, nil } type soPlugin struct { - handle unsafe.Pointer - onLoadFn unsafe.Pointer - onEventFn unsafe.Pointer - onUnloadFn unsafe.Pointer - cbHandle cgo.Handle - mu sync.Mutex + handle unsafe.Pointer + onLoadFn unsafe.Pointer + onEventFn unsafe.Pointer + onUnloadFn unsafe.Pointer + cbHandle cgo.Handle + pluginRuntime string + mu sync.Mutex } func (p *soPlugin) Load(send func(string, []byte), hostInfo []byte) error { @@ -220,8 +234,14 @@ func (p *soPlugin) Close() error { p.cbHandle = 0 } if p.handle != nil { - C.so_dlclose(p.handle) + if p.pluginRuntime != "go" { + C.so_dlclose(p.handle) + } p.handle = nil } return nil } + +func (p *soPlugin) Runtime() string { + return p.pluginRuntime +} diff --git a/Overlord-Client/cmd/agent/plugins/loader_windows.go b/Overlord-Client/cmd/agent/plugins/loader_windows.go index b5b09f2..977a38d 100644 --- a/Overlord-Client/cmd/agent/plugins/loader_windows.go +++ b/Overlord-Client/cmd/agent/plugins/loader_windows.go @@ -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 - p.mem = nil + 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 +} diff --git a/Overlord-Client/cmd/agent/plugins/manager.go b/Overlord-Client/cmd/agent/plugins/manager.go index 96ab8d8..5f6bd5b 100644 --- a/Overlord-Client/cmd/agent/plugins/manager.go +++ b/Overlord-Client/cmd/agent/plugins/manager.go @@ -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) + } } } diff --git a/Overlord-Client/cmd/agent/plugins/types.go b/Overlord-Client/cmd/agent/plugins/types.go index cd8c9a6..810863e 100644 --- a/Overlord-Client/cmd/agent/plugins/types.go +++ b/Overlord-Client/cmd/agent/plugins/types.go @@ -41,6 +41,8 @@ type NativePlugin interface { Unload() Close() error + + Runtime() string } func ManifestFromMap(m map[string]interface{}) (PluginManifest, error) { diff --git a/build-plugin-c.bat b/build-plugin-c.bat new file mode 100644 index 0000000..3a00317 --- /dev/null +++ b/build-plugin-c.bat @@ -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% diff --git a/build-plugin-c.sh b/build-plugin-c.sh new file mode 100644 index 0000000..3e1319b --- /dev/null +++ b/build-plugin-c.sh @@ -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}" diff --git a/build-plugin-cpp.bat b/build-plugin-cpp.bat new file mode 100644 index 0000000..a7c829e --- /dev/null +++ b/build-plugin-cpp.bat @@ -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% diff --git a/build-plugin-cpp.sh b/build-plugin-cpp.sh new file mode 100644 index 0000000..78b5aae --- /dev/null +++ b/build-plugin-cpp.sh @@ -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}" diff --git a/build-plugin-rust.bat b/build-plugin-rust.bat new file mode 100644 index 0000000..68c04be --- /dev/null +++ b/build-plugin-rust.bat @@ -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//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% diff --git a/build-plugin-rust.sh b/build-plugin-rust.sh new file mode 100644 index 0000000..b66931b --- /dev/null +++ b/build-plugin-rust.sh @@ -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}" diff --git a/plugin-sample-c/native/plugin.c b/plugin-sample-c/native/plugin.c new file mode 100644 index 0000000..ceb9d2b --- /dev/null +++ b/plugin-sample-c/native/plugin.c @@ -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 +#include +#include +#include + +/* ------------------------------------------------------------------ */ +/* Platform helpers */ +/* ------------------------------------------------------------------ */ + +#ifdef _WIN32 +# define EXPORT __declspec(dllexport) +# define WIN32_LEAN_AND_MEAN +# include + 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'; +} diff --git a/plugin-sample-c/sample-c.css b/plugin-sample-c/sample-c.css new file mode 100644 index 0000000..3e52e33 --- /dev/null +++ b/plugin-sample-c/sample-c.css @@ -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; +} diff --git a/plugin-sample-c/sample-c.html b/plugin-sample-c/sample-c.html new file mode 100644 index 0000000..6f917d6 --- /dev/null +++ b/plugin-sample-c/sample-c.html @@ -0,0 +1,41 @@ + + + + + + Sample C Plugin + + + +
+
+

Sample C Plugin

+

Pure C — fully unloadable, zero runtime leak

+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+

Log

+

+      
+
+ + + + diff --git a/plugin-sample-c/sample-c.js b/plugin-sample-c/sample-c.js new file mode 100644 index 0000000..e1f89e3 --- /dev/null +++ b/plugin-sample-c/sample-c.js @@ -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)"); + } +} diff --git a/plugin-sample-cpp/native/plugin.cpp b/plugin-sample-cpp/native/plugin.cpp new file mode 100644 index 0000000..1c7cdbd --- /dev/null +++ b/plugin-sample-cpp/native/plugin.cpp @@ -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 +#include +#include +#include +#include +#include + +/* ------------------------------------------------------------------ */ +/* Platform helpers */ +/* ------------------------------------------------------------------ */ + +#ifdef _WIN32 +# define EXPORT extern "C" __declspec(dllexport) +# define WIN32_LEAN_AND_MEAN +# include + 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(strlen(event)) : 0; + int plen = payload ? static_cast(strlen(payload)) : 0; +#ifdef _WIN32 + g_callback(event, static_cast(elen), + payload, static_cast(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 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(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(static_cast(cb)); +} + +EXPORT int PluginOnLoad(const char *hostInfo, int hostInfoLen, uint64_t cb) { + g_callback = reinterpret_cast(static_cast(cb)); +#else +EXPORT int PluginOnLoad(const char *hostInfo, int hostInfoLen, + uintptr_t cb, uintptr_t ctx) { + g_callback = reinterpret_cast(cb); + g_callback_ctx = ctx; +#endif + + { + std::lock_guard 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(eventLen)); + + { + std::lock_guard 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(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 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 lk(g_mu); + g_client_id.clear(); + g_event_counts.clear(); +} diff --git a/plugin-sample-cpp/sample-cpp.css b/plugin-sample-cpp/sample-cpp.css new file mode 100644 index 0000000..3e52e33 --- /dev/null +++ b/plugin-sample-cpp/sample-cpp.css @@ -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; +} diff --git a/plugin-sample-cpp/sample-cpp.html b/plugin-sample-cpp/sample-cpp.html new file mode 100644 index 0000000..6d529cd --- /dev/null +++ b/plugin-sample-cpp/sample-cpp.html @@ -0,0 +1,42 @@ + + + + + + Sample C++ Plugin + + + +
+
+

Sample C++ Plugin

+

C++ — fully unloadable, zero runtime leak

+
+ +
+
+ + +
+ +
+ + +
+ +
+ + + +
+
+ +
+

Log

+

+      
+
+ + + + diff --git a/plugin-sample-cpp/sample-cpp.js b/plugin-sample-cpp/sample-cpp.js new file mode 100644 index 0000000..562ace0 --- /dev/null +++ b/plugin-sample-cpp/sample-cpp.js @@ -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)"); + } +} diff --git a/plugin-sample-rust/native/Cargo.toml b/plugin-sample-rust/native/Cargo.toml new file mode 100644 index 0000000..77a07aa --- /dev/null +++ b/plugin-sample-rust/native/Cargo.toml @@ -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 diff --git a/plugin-sample-rust/native/src/lib.rs b/plugin-sample-rust/native/src/lib.rs new file mode 100644 index 0000000..4d04a5c --- /dev/null +++ b/plugin-sample-rust/native/src/lib.rs @@ -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 = 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 +} diff --git a/plugin-sample-rust/sample-rust.css b/plugin-sample-rust/sample-rust.css new file mode 100644 index 0000000..3e52e33 --- /dev/null +++ b/plugin-sample-rust/sample-rust.css @@ -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; +} diff --git a/plugin-sample-rust/sample-rust.html b/plugin-sample-rust/sample-rust.html new file mode 100644 index 0000000..1360a6e --- /dev/null +++ b/plugin-sample-rust/sample-rust.html @@ -0,0 +1,42 @@ + + + + + + Sample Rust Plugin + + + +
+
+

Sample Rust Plugin

+

Rust — fully unloadable, zero runtime leak

+
+ +
+
+ + +
+ +
+ + +
+ +
+ + + +
+
+ +
+

Log

+

+      
+
+ + + + diff --git a/plugin-sample-rust/sample-rust.js b/plugin-sample-rust/sample-rust.js new file mode 100644 index 0000000..ee87ed6 --- /dev/null +++ b/plugin-sample-rust/sample-rust.js @@ -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)"); + } +}