Files
HomeAgent/internal/plugin/dynamic_dll_windows.go

273 lines
6.7 KiB
Go

//go:build windows
package plugin
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"syscall"
"unsafe"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
)
// dllPlugin wraps a Windows DLL compiled with -buildmode=c-shared.
//
// Required exports:
//
// NewPlugin(name *C.char, configJSON *C.char) unsafe.Pointer → plugin handle
// StartPlugin(handle unsafe.Pointer) C.int
// StopPlugin(handle unsafe.Pointer) C.int
// DestroyPlugin(handle unsafe.Pointer)
//
// Optional exports (tool registration):
//
// GetToolDefsJSON(handle unsafe.Pointer) *C.char → JSON array of tool defs
// InvokeToolJSON(handle unsafe.Pointer, toolName *C.char, argsJSON *C.char) *C.char
// FreeCString(s *C.char) → free C string from DLL
// GetStagesJSON(handle unsafe.Pointer) *C.char → JSON array of stage names
// InvokeStage(handle unsafe.Pointer, stage *C.char, contextJSON *C.char) C.int
type dllPlugin struct {
name string
dll syscall.Handle
handle uintptr
sdk *sdk.PluginSDK
// cached proc addresses
newPlugin uintptr
startPlugin uintptr
stopPlugin uintptr
destroyPlugin uintptr
getTools uintptr
invokeTool uintptr
freeCStr uintptr
getStages uintptr
invokeStage uintptr
}
func findProc(dll syscall.Handle, name string) uintptr {
addr, err := syscall.GetProcAddress(dll, name)
if err != nil {
return 0
}
return addr
}
func newDLLPlugin(dllPath, name string, config map[string]interface{}) (*dllPlugin, error) {
dll, err := syscall.LoadLibrary(dllPath)
if err != nil {
return nil, fmt.Errorf("LoadLibrary %s: %w", dllPath, err)
}
np := findProc(dll, "NewPlugin")
if np == 0 {
_ = syscall.FreeLibrary(dll)
return nil, fmt.Errorf("dll %s must export NewPlugin", name)
}
return &dllPlugin{
name: name,
dll: dll,
// required
newPlugin: np,
startPlugin: findProc(dll, "StartPlugin"),
stopPlugin: findProc(dll, "StopPlugin"),
destroyPlugin: findProc(dll, "DestroyPlugin"),
// optional tool/stage API
getTools: findProc(dll, "GetToolDefsJSON"),
invokeTool: findProc(dll, "InvokeToolJSON"),
freeCStr: findProc(dll, "FreeCString"),
getStages: findProc(dll, "GetStagesJSON"),
invokeStage: findProc(dll, "InvokeStage"),
}, nil
}
func (p *dllPlugin) Name() string { return p.name }
func (p *dllPlugin) Start(s *sdk.PluginSDK) error {
p.sdk = s
cfgJSON, _ := json.Marshal(map[string]interface{}{
"name": p.name,
"config": s.Settings().Dump(),
})
cName := append([]byte(p.name), 0)
cConfig := append(cfgJSON, 0)
ret, _, _ := syscall.SyscallN(
p.newPlugin,
uintptr(unsafe.Pointer(&cName[0])),
uintptr(unsafe.Pointer(&cConfig[0])),
)
if ret == 0 {
_ = syscall.FreeLibrary(p.dll)
return fmt.Errorf("dll NewPlugin %s returned nil", p.name)
}
p.handle = ret
if p.startPlugin != 0 {
syscall.SyscallN(p.startPlugin, p.handle)
}
// discover and register tools from DLL
if p.getTools != 0 {
if err := p.registerTools(s); err != nil {
return fmt.Errorf("dll %s register tools: %w", p.name, err)
}
}
if p.getStages != 0 {
p.registerStages(s)
}
return nil
}
func (p *dllPlugin) Stop() error {
if p.stopPlugin != 0 {
syscall.SyscallN(p.stopPlugin, p.handle)
}
if p.destroyPlugin != 0 {
syscall.SyscallN(p.destroyPlugin, p.handle)
}
_ = syscall.FreeLibrary(p.dll)
return nil
}
// --- tool registration via C ABI ---
type dllToolDef struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters map[string]interface{} `json:"parameters,omitempty"`
}
func (p *dllPlugin) registerTools(s *sdk.PluginSDK) error {
ret, _, _ := syscall.SyscallN(p.getTools, p.handle)
if ret == 0 {
return nil // no tools
}
defsJSON := cStringPtrToString(ret)
if p.freeCStr != 0 {
syscall.SyscallN(p.freeCStr, ret)
}
var defs []dllToolDef
if err := json.Unmarshal([]byte(defsJSON), &defs); err != nil {
return fmt.Errorf("parse tool defs: %w", err)
}
for _, d := range defs {
if d.Name == "" {
continue
}
toolName := d.Name
handler := p.makeToolHandler(toolName)
s.RegisterTool(toolName, sdk.ToolDef{
Name: toolName,
Description: d.Description,
Parameters: d.Parameters,
Plugin: p.name,
}, handler)
}
return nil
}
func (p *dllPlugin) makeToolHandler(toolName string) sdk.ToolHandler {
return func(args map[string]interface{}) (interface{}, error) {
if p.invokeTool == 0 {
return nil, fmt.Errorf("dll %s does not export InvokeToolJSON", p.name)
}
argsJSON, _ := json.Marshal(args)
cToolName := append([]byte(toolName), 0)
cArgs := append(argsJSON, 0)
ret, _, _ := syscall.SyscallN(
p.invokeTool,
p.handle,
uintptr(unsafe.Pointer(&cToolName[0])),
uintptr(unsafe.Pointer(&cArgs[0])),
)
if ret == 0 {
return nil, fmt.Errorf("dll InvokeToolJSON %s returned nil", toolName)
}
resultJSON := cStringPtrToString(ret)
if p.freeCStr != 0 {
syscall.SyscallN(p.freeCStr, ret)
}
var result map[string]interface{}
if err := json.Unmarshal([]byte(resultJSON), &result); err != nil {
return nil, fmt.Errorf("dll tool %s result parse: %w", toolName, err)
}
return result, nil
}
}
func (p *dllPlugin) registerStages(s *sdk.PluginSDK) {
ret, _, _ := syscall.SyscallN(p.getStages, p.handle)
if ret == 0 {
return
}
stagesJSON := cStringPtrToString(ret)
if p.freeCStr != 0 {
syscall.SyscallN(p.freeCStr, ret)
}
type stageEntry struct {
Stage string `json:"stage"`
}
var entries []stageEntry
if err := json.Unmarshal([]byte(stagesJSON), &entries); err != nil {
return
}
for _, e := range entries {
if e.Stage == "" {
continue
}
stageName := sdk.Stage(e.Stage)
stage := stageName
s.RegisterStage(stage, func(sc *sdk.StageContext) error {
if p.invokeStage == 0 {
return nil
}
ctxJSON, _ := json.Marshal(map[string]interface{}{
"raw_message": sc.RawMessage,
"user_id": sc.UserID,
"phase": string(sc.Phase),
})
cStage := append([]byte(stage), 0)
cCtx := append(ctxJSON, 0)
syscall.SyscallN(
p.invokeStage,
p.handle,
uintptr(unsafe.Pointer(&cStage[0])),
uintptr(unsafe.Pointer(&cCtx[0])),
)
return nil
})
}
}
func cStringPtrToString(ptr uintptr) string {
if ptr == 0 {
return ""
}
var buf []byte
for i := uintptr(0); ; i++ {
b := *(*byte)(unsafe.Pointer(ptr + i))
if b == 0 {
break
}
buf = append(buf, b)
}
return string(buf)
}
// tryLoadDLL 尝试从插件目录加载 plugin.dll。
// 返回 nil,nil 表示目录中没有 plugin.dll。
func tryLoadDLL(dir, name string, config map[string]interface{}) (sdk.Plugin, error) {
dllPath := filepath.Join(dir, dllEntry)
if _, err := os.Stat(dllPath); os.IsNotExist(err) {
return nil, nil
}
return newDLLPlugin(dllPath, name, config)
}