2026-04-19 15:07:44 -05:00
|
|
|
//go:build darwin && cgo && !ios && !ios_target
|
2026-03-19 16:29:26 -05:00
|
|
|
|
|
|
|
|
package plugins
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
#include <dlfcn.h>
|
|
|
|
|
#include <stdint.h>
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
|
|
|
|
|
static void* so_dlopen(const char* path, char** errOut) {
|
|
|
|
|
void* h = dlopen(path, RTLD_NOW | RTLD_LOCAL);
|
|
|
|
|
if (!h && errOut) *errOut = (char*)dlerror();
|
|
|
|
|
return h;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void* so_dlsym(void* h, const char* name, char** errOut) {
|
|
|
|
|
void* s = dlsym(h, name);
|
|
|
|
|
if (!s && errOut) *errOut = (char*)dlerror();
|
|
|
|
|
return s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int so_dlclose(void* h) {
|
|
|
|
|
if (h) return dlclose(h);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Go callback bridge — forward-declared here, implemented via //export below.
|
|
|
|
|
extern void goPluginHostCallback(
|
|
|
|
|
uintptr_t ctx,
|
2026-03-19 18:43:10 -05:00
|
|
|
char* event, int eventLen,
|
|
|
|
|
char* payload, int payloadLen);
|
2026-03-19 16:29:26 -05:00
|
|
|
|
|
|
|
|
// Call the plugin's PluginOnLoad, injecting our callback bridge + context.
|
|
|
|
|
static int so_call_onload(void* fn, const char* info, int infoLen, uintptr_t ctx) {
|
|
|
|
|
typedef int (*fn_t)(const char*, int, uintptr_t, uintptr_t);
|
|
|
|
|
return ((fn_t)fn)(info, infoLen, (uintptr_t)goPluginHostCallback, ctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int so_call_onevent(void* fn, const char* ev, int evLen, const char* pl, int plLen) {
|
|
|
|
|
typedef int (*fn_t)(const char*, int, const char*, int);
|
|
|
|
|
return ((fn_t)fn)(ev, evLen, pl, plLen);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void so_call_onunload(void* fn) {
|
|
|
|
|
typedef void (*fn_t)(void);
|
|
|
|
|
((fn_t)fn)();
|
|
|
|
|
}
|
2026-04-04 20:06:23 -05:00
|
|
|
|
|
|
|
|
static const char* so_call_getruntime(void* fn) {
|
|
|
|
|
typedef const char* (*fn_t)(void);
|
|
|
|
|
return ((fn_t)fn)();
|
|
|
|
|
}
|
2026-03-19 16:29:26 -05:00
|
|
|
*/
|
|
|
|
|
import "C"
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"os"
|
|
|
|
|
"runtime/cgo"
|
|
|
|
|
"sync"
|
|
|
|
|
"unsafe"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
//export goPluginHostCallback
|
|
|
|
|
func goPluginHostCallback(ctx C.uintptr_t, event *C.char, eventLen C.int, payload *C.char, payloadLen C.int) {
|
|
|
|
|
h := cgo.Handle(ctx)
|
|
|
|
|
send := h.Value().(func(string, []byte))
|
|
|
|
|
ev := C.GoStringN(event, eventLen)
|
|
|
|
|
var pl []byte
|
|
|
|
|
if payloadLen > 0 {
|
|
|
|
|
pl = C.GoBytes(unsafe.Pointer(payload), payloadLen)
|
|
|
|
|
}
|
|
|
|
|
send(ev, pl)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func loadNativePlugin(data []byte) (NativePlugin, error) {
|
|
|
|
|
if len(data) == 0 {
|
|
|
|
|
return nil, errors.New("empty plugin binary")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
f, err := os.CreateTemp("", "*.dylib")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("create temp: %w", err)
|
|
|
|
|
}
|
|
|
|
|
tmpPath := f.Name()
|
|
|
|
|
|
|
|
|
|
if _, err := f.Write(data); err != nil {
|
|
|
|
|
f.Close()
|
|
|
|
|
os.Remove(tmpPath)
|
|
|
|
|
return nil, fmt.Errorf("write temp: %w", err)
|
|
|
|
|
}
|
|
|
|
|
f.Close()
|
|
|
|
|
|
|
|
|
|
cPath := C.CString(tmpPath)
|
|
|
|
|
defer C.free(unsafe.Pointer(cPath))
|
|
|
|
|
var cErr *C.char
|
|
|
|
|
handle := C.so_dlopen(cPath, &cErr)
|
|
|
|
|
|
|
|
|
|
os.Remove(tmpPath)
|
|
|
|
|
|
|
|
|
|
if handle == nil {
|
|
|
|
|
return nil, fmt.Errorf("dlopen: %s", C.GoString(cErr))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resolve := func(name string) (unsafe.Pointer, error) {
|
|
|
|
|
cName := C.CString(name)
|
|
|
|
|
defer C.free(unsafe.Pointer(cName))
|
|
|
|
|
var cErr *C.char
|
|
|
|
|
s := C.so_dlsym(handle, cName, &cErr)
|
|
|
|
|
if s == nil {
|
|
|
|
|
return nil, fmt.Errorf("dlsym(%s): %s", name, C.GoString(cErr))
|
|
|
|
|
}
|
|
|
|
|
return s, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onLoad, err := resolve("PluginOnLoad")
|
|
|
|
|
if err != nil {
|
|
|
|
|
C.so_dlclose(handle)
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
onEvent, err := resolve("PluginOnEvent")
|
|
|
|
|
if err != nil {
|
|
|
|
|
C.so_dlclose(handle)
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
onUnload, err := resolve("PluginOnUnload")
|
|
|
|
|
if err != nil {
|
|
|
|
|
C.so_dlclose(handle)
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 20:06:23 -05:00
|
|
|
pluginRT := "go"
|
|
|
|
|
if getRuntimeFn, err := resolve("PluginGetRuntime"); err == nil {
|
|
|
|
|
if cs := C.so_call_getruntime(getRuntimeFn); cs != nil {
|
|
|
|
|
pluginRT = C.GoString(cs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 16:29:26 -05:00
|
|
|
return &soPlugin{
|
2026-04-04 20:06:23 -05:00
|
|
|
handle: handle,
|
|
|
|
|
onLoadFn: onLoad,
|
|
|
|
|
onEventFn: onEvent,
|
|
|
|
|
onUnloadFn: onUnload,
|
|
|
|
|
pluginRuntime: pluginRT,
|
2026-03-19 16:29:26 -05:00
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type soPlugin struct {
|
2026-04-04 20:06:23 -05:00
|
|
|
handle unsafe.Pointer
|
|
|
|
|
onLoadFn unsafe.Pointer
|
|
|
|
|
onEventFn unsafe.Pointer
|
|
|
|
|
onUnloadFn unsafe.Pointer
|
|
|
|
|
cbHandle cgo.Handle
|
|
|
|
|
pluginRuntime string
|
|
|
|
|
mu sync.Mutex
|
2026-03-19 16:29:26 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *soPlugin) Load(send func(string, []byte), hostInfo []byte) error {
|
|
|
|
|
p.mu.Lock()
|
|
|
|
|
defer p.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
p.cbHandle = cgo.NewHandle(send)
|
|
|
|
|
|
|
|
|
|
var infoPtr *C.char
|
|
|
|
|
if len(hostInfo) > 0 {
|
|
|
|
|
infoPtr = (*C.char)(unsafe.Pointer(&hostInfo[0]))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ret := C.so_call_onload(p.onLoadFn, infoPtr, C.int(len(hostInfo)), C.uintptr_t(p.cbHandle))
|
|
|
|
|
if ret != 0 {
|
|
|
|
|
p.cbHandle.Delete()
|
|
|
|
|
p.cbHandle = 0
|
|
|
|
|
return errors.New("PluginOnLoad returned error")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *soPlugin) Event(event string, payload []byte) error {
|
|
|
|
|
p.mu.Lock()
|
|
|
|
|
defer p.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
evBytes := []byte(event)
|
|
|
|
|
var evPtr, plPtr *C.char
|
|
|
|
|
if len(evBytes) > 0 {
|
|
|
|
|
evPtr = (*C.char)(unsafe.Pointer(&evBytes[0]))
|
|
|
|
|
}
|
|
|
|
|
if len(payload) > 0 {
|
|
|
|
|
plPtr = (*C.char)(unsafe.Pointer(&payload[0]))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ret := C.so_call_onevent(p.onEventFn, evPtr, C.int(len(evBytes)), plPtr, C.int(len(payload)))
|
|
|
|
|
if ret != 0 {
|
|
|
|
|
return errors.New("PluginOnEvent returned error")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *soPlugin) Unload() {
|
|
|
|
|
p.mu.Lock()
|
|
|
|
|
defer p.mu.Unlock()
|
|
|
|
|
if p.onUnloadFn != nil {
|
|
|
|
|
C.so_call_onunload(p.onUnloadFn)
|
|
|
|
|
p.onUnloadFn = nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *soPlugin) Close() error {
|
|
|
|
|
p.Unload()
|
|
|
|
|
p.mu.Lock()
|
|
|
|
|
defer p.mu.Unlock()
|
|
|
|
|
if p.cbHandle != 0 {
|
|
|
|
|
p.cbHandle.Delete()
|
|
|
|
|
p.cbHandle = 0
|
|
|
|
|
}
|
|
|
|
|
if p.handle != nil {
|
2026-04-04 20:06:23 -05:00
|
|
|
if p.pluginRuntime != "go" {
|
|
|
|
|
C.so_dlclose(p.handle)
|
|
|
|
|
}
|
2026-03-19 16:29:26 -05:00
|
|
|
p.handle = nil
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-04-04 20:06:23 -05:00
|
|
|
|
|
|
|
|
func (p *soPlugin) Runtime() string {
|
|
|
|
|
return p.pluginRuntime
|
|
|
|
|
}
|