mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-10-15 12:15:11 +00:00
v0.9.0: C ABI v2 stage 写回 + ABI 版本对齐核心版本号
- meta: ABI 标识版本改为字符串 semver(ABIVersion=CoreVersion="0.9.0"), C 层协商用派生整数 CABINum=900(major*100+minor),不再用独立数字编码 - plugindev 模板: invoke_stage 增加 result out 参数(stage 写回), 插件在 OnInput/AfterToolcall/PostAction 修改 StageContext 后回传内核 - cmd_init: CABIVersion 改用 CABINum - example/sanitizer: 增强为全链路清洗(坏 UTF-8/U+FFFD/ANSI 转义), 挂载 OnInput/AfterToolcall/PostAction 三阶段(依赖 stage 写回能力)
This commit is contained in:
@ -1,5 +1,13 @@
|
||||
// Package main 是一个外部插件示例(编译为 .so 通过 -buildmode=plugin)。
|
||||
// 在 StagePostAction 阶段清洗 LLM 输出中的工具调用残留(思维泄漏)。
|
||||
// 目标:在 Agent 全链路清洗文本,防止乱码(坏 UTF-8 / U+FFFD / ANSI 转义)污染上下文并被 LLM 复读,
|
||||
// 同时保留原有"工具调用残留(思维泄漏)"清理。
|
||||
//
|
||||
// 挂载阶段:
|
||||
// - StageOnInput : 清洗用户输入(RawMessage)
|
||||
// - StageAfterToolcall : 清洗工具执行结果(ToolResults),坏字节不进 LLM 上下文
|
||||
// - StagePostAction : 清洗 LLM 输出(LLMText),保留原有思维泄漏清理
|
||||
//
|
||||
// 依赖 ABI v2 的 stage 写回能力:插件对 StageContext 的修改会同步回内核。
|
||||
//
|
||||
// 编译:
|
||||
//
|
||||
@ -13,23 +21,24 @@ import (
|
||||
"log"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||||
)
|
||||
|
||||
var (
|
||||
toolCallTagRE = regexp.MustCompile(`(?s)<tool_call[^>]*>.*?</tool_call>`)
|
||||
invokeTagRE = regexp.MustCompile(`(?s)<invoke[^>]*>.*?</invoke>`)
|
||||
toolTagRE = regexp.MustCompile(`(?s)<tool[^>]*>.*?</tool>`)
|
||||
functionTagRE = regexp.MustCompile(`(?s)<function[^>]*>.*?</function>`)
|
||||
toolCodeBlockRE = regexp.MustCompile("(?s)```(?:xml|json)?\\s*<tool_call[^>]*>.*?</tool_call>\\s*```")
|
||||
toolCallTagRE = regexp.MustCompile(`(?s)<tool_call[^>]*>.*?</tool_call>`)
|
||||
invokeTagRE = regexp.MustCompile(`(?s)<invoke[^>]*>.*?</invoke>`)
|
||||
toolTagRE = regexp.MustCompile(`(?s)<tool[^>]*>.*?</tool>`)
|
||||
functionTagRE = regexp.MustCompile(`(?s)<function[^>]*>.*?</function>`)
|
||||
toolCodeBlockRE = regexp.MustCompile("(?s)```(?:xml|json)?\\s*<tool_call[^>]*>.*?</tool_call>\\s*```")
|
||||
invokeCodeBlockRE = regexp.MustCompile("(?s)```(?:xml|json)?\\s*<invoke[^>]*>.*?</invoke>\\s*```")
|
||||
toolCodeBlockRE2 = regexp.MustCompile("(?s)```(?:xml|json)?\\s*<tool[^>]*>.*?</tool>\\s*```")
|
||||
chineseMarkerRE = regexp.MustCompile(`(?s)【tool_call】.*?【/tool_call】`)
|
||||
multiNewlineRE = regexp.MustCompile(`\n{3,}`)
|
||||
toolNameRE = regexp.MustCompile(`^(cmd_run|terminal_create|terminal_write|memory_|knowledge_|doc_|social_|output_set_channel|output_send|llm_|plgreload|spawn_child|child_result|describe_image|transcribe_audio|ocr_image|timer_set|plugin_install|plugin_remove|qq_|a2a_|mcp_|healthcheck|files_|web_)`)
|
||||
placeholderRE = regexp.MustCompile(`(?i)\{\{\s*tool\s*[::][^}]*\}\}`)
|
||||
atToolRE = regexp.MustCompile(`(?i)^@\s*tool\b`)
|
||||
toolCodeBlockRE2 = regexp.MustCompile("(?s)```(?:xml|json)?\\s*<tool[^>]*>.*?</tool>\\s*```")
|
||||
chineseMarkerRE = regexp.MustCompile(`(?s)【tool_call】.*?【/tool_call】`)
|
||||
multiNewlineRE = regexp.MustCompile(`\n{3,}`)
|
||||
toolNameRE = regexp.MustCompile(`^(cmd_run|terminal_create|terminal_write|memory_|knowledge_|doc_|social_|output_set_channel|output_send|llm_|plgreload|spawn_child|child_result|describe_image|transcribe_audio|ocr_image|timer_set|plugin_install|plugin_remove|qq_|a2a_|mcp_|healthcheck|files_|web_)`)
|
||||
placeholderRE = regexp.MustCompile(`(?i)\{\{\s*tool\s*[::][^}]*\}\}`)
|
||||
atToolRE = regexp.MustCompile(`(?i)^@\s*tool\b`)
|
||||
)
|
||||
|
||||
type Plugin struct{}
|
||||
@ -38,10 +47,41 @@ func (p *Plugin) Name() string { return "sanitizer" }
|
||||
|
||||
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
s.SetAutoRestart(true)
|
||||
|
||||
// 1) 输入清洗
|
||||
s.RegisterStage(sdk.StageOnInput, func(ctx *sdk.StageContext) error {
|
||||
ctx.Lock()
|
||||
before := ctx.RawMessage
|
||||
ctx.RawMessage = cleanText(ctx.RawMessage)
|
||||
if before != ctx.RawMessage {
|
||||
log.Printf("[sanitizer] StageOnInput: cleaned %d bytes", len(before)-len(ctx.RawMessage))
|
||||
}
|
||||
ctx.Unlock()
|
||||
return nil
|
||||
})
|
||||
|
||||
// 2) 工具结果清洗(坏字节/ANSI 不得进 LLM 上下文)
|
||||
s.RegisterStage(sdk.StageAfterToolcall, func(ctx *sdk.StageContext) error {
|
||||
ctx.Lock()
|
||||
defer ctx.Unlock()
|
||||
for i, tr := range ctx.ToolResults {
|
||||
if s, ok := tr.Result.(string); ok {
|
||||
clean := cleanText(s)
|
||||
if clean != s {
|
||||
ctx.ToolResults[i].Result = clean
|
||||
log.Printf("[sanitizer] StageAfterToolcall: tool=%s cleaned %d bytes", tr.Name, len(s)-len(clean))
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// 3) LLM 输出清洗(保留原有思维泄漏清理 + 新增乱码清洗)
|
||||
s.RegisterStage(sdk.StagePostAction, func(ctx *sdk.StageContext) error {
|
||||
ctx.Lock()
|
||||
before := len(ctx.LLMText)
|
||||
ctx.LLMText = cleanToolCallLeakage(ctx.LLMText)
|
||||
ctx.LLMText = cleanText(ctx.LLMText)
|
||||
after := len(ctx.LLMText)
|
||||
ctx.Unlock()
|
||||
if before != after {
|
||||
@ -49,7 +89,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
}
|
||||
return nil
|
||||
})
|
||||
log.Printf("[sanitizer] stage PostAction registered")
|
||||
log.Printf("[sanitizer] stage OnInput/AfterToolcall/PostAction registered")
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -59,6 +99,7 @@ func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, e
|
||||
return &Plugin{}, nil
|
||||
}
|
||||
|
||||
// cleanToolCallLeakage 清洗 LLM 输出中的工具调用残留(思维泄漏)。
|
||||
func cleanToolCallLeakage(content string) string {
|
||||
if content == "" {
|
||||
return content
|
||||
@ -106,3 +147,72 @@ func cleanToolCallLeakage(content string) string {
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
// cleanText 清洗可能污染 LLM 上下文/输出的文本:
|
||||
// 1. 剥离 ANSI 转义序列(\x1b[...m 等,源自终端输出)
|
||||
// 2. 剔除无效 UTF-8 字节(strings.ToValidUTF8 语义)与已解码的 U+FFFD 替换符,
|
||||
// 避免模型复读坏字节/替换符造成乱码(把坏段落整体丢弃比留残字更干净)
|
||||
func cleanText(s string) string {
|
||||
if s == "" {
|
||||
return s
|
||||
}
|
||||
// 先剥离 ANSI 转义:ESC [ 参数 m / ESC ] 标题 / 其他 CSI 序列
|
||||
if strings.ContainsRune(s, 0x1b) {
|
||||
var sb strings.Builder
|
||||
sb.Grow(len(s))
|
||||
i := 0
|
||||
for i < len(s) {
|
||||
c := s[i]
|
||||
if c == 0x1b {
|
||||
// 跳过完整转义序列
|
||||
j := i + 1
|
||||
if j < len(s) {
|
||||
switch s[j] {
|
||||
case '[': // CSI: ESC [ <params> <letter>
|
||||
j++
|
||||
for j < len(s) && !(s[j] >= 0x40 && s[j] <= 0x7e) {
|
||||
j++
|
||||
}
|
||||
if j < len(s) {
|
||||
j++
|
||||
}
|
||||
i = j
|
||||
continue
|
||||
case ']': // OSC: ESC ] ... BEL / ST
|
||||
i = j + 1
|
||||
for i < len(s) && s[i] != 0x07 {
|
||||
i++
|
||||
}
|
||||
i++ // skip BEL
|
||||
continue
|
||||
default: // 单字符转义(ESC c ESC 7 等)
|
||||
i = j + 1
|
||||
continue
|
||||
}
|
||||
}
|
||||
i++
|
||||
continue
|
||||
}
|
||||
sb.WriteByte(c)
|
||||
i++
|
||||
}
|
||||
s = sb.String()
|
||||
}
|
||||
|
||||
// 剔除无效 UTF-8 与 U+FFFD 替换符
|
||||
if !utf8.ValidString(s) {
|
||||
s = strings.ToValidUTF8(s, "")
|
||||
}
|
||||
if strings.ContainsRune(s, utf8.RuneError) {
|
||||
// 连 U+FFFD 也不留给模型复述
|
||||
var b strings.Builder
|
||||
b.Grow(len(s))
|
||||
for _, r := range s {
|
||||
if r != utf8.RuneError {
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
s = b.String()
|
||||
}
|
||||
return s
|
||||
}
|
||||
@ -2,6 +2,31 @@ package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCleanText(t *testing.T) {
|
||||
tests := []struct {
|
||||
name, input, want string
|
||||
}{
|
||||
{"empty", "", ""},
|
||||
{"clean", "你好世界 hello", "你好世界 hello"},
|
||||
{"invalid_utf8", "a\xff\xfe b", "a b"},
|
||||
{"ufffd", "有乱码\ufffd字符", "有乱码字符"},
|
||||
{"multiple_ufffd", "a\ufffd\ufffdb\ufffdc", "abc"},
|
||||
{"ansi_color", "\x1b[31m红色\x1b[0m结束", "红色结束"},
|
||||
{"ansi_cursor", "a\x1b[2K\r\nb", "a\r\nb"},
|
||||
{"ansi_osc", "\x1b]0;title\x07文本", "文本"},
|
||||
{"an_and_ufffd", "\x1b[31m\ufffd中文\x1b[0m", "中文"},
|
||||
{"emoji_kept", "颜文字(・ω・´)和🍎", "颜文字(・ω・´)和🍎"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := cleanText(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("got %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanToolCallLeakage(t *testing.T) {
|
||||
tests := []struct {
|
||||
name, input, want string
|
||||
|
||||
22
meta/meta.go
22
meta/meta.go
@ -6,7 +6,7 @@ package meta
|
||||
var (
|
||||
// Version 是 HomeAgent SDK 版本号。
|
||||
// 通过 `-ldflags="-X gitcode.com/JianFeeeee/homeagent-sdk/meta.Version=vX.Y.Z"` 注入。
|
||||
Version = "0.8.0"
|
||||
Version = "0.9.0"
|
||||
|
||||
// Commit 是构建时的 Git commit hash。
|
||||
Commit = "unknown"
|
||||
@ -21,7 +21,7 @@ var (
|
||||
CoreModule = "gitcode.com/JianFeeeee/HomeAgent"
|
||||
|
||||
// CoreVersion 是此 SDK 所兼容的最低核心版本。
|
||||
CoreVersion = "0.8.0"
|
||||
CoreVersion = "0.9.0"
|
||||
)
|
||||
|
||||
// FullVersion 返回完整的版本字符串。
|
||||
@ -30,11 +30,23 @@ func FullVersion() string {
|
||||
}
|
||||
|
||||
// ---- ABI 版本(与核心仓 internal/meta/meta.go 同步) ----
|
||||
// 修改时需确保核心仓与 SDK 仓的值一致。
|
||||
// ABI 标识版本直接取内核版本号字符串(semver),与核心 Version 保持一致,不使用独立数字编码。
|
||||
// 协商层(C 结构体 int version 字段)使用 CABINum:由版本字符串派生的整数(major*100 + minor)。
|
||||
// 映射:v0.8.x → CABINum=800;v0.9.x → CABINum=900(invoke_stage 写回)。
|
||||
// 小版本(patch)演进不影响 ABI,CABINum 不变。version_min 保证旧 ABI 插件仍可加载。
|
||||
|
||||
var (
|
||||
// ABIVersion 是 ABI 标识版本(字符串 semver,与 SDK CoreVersion 对齐)。
|
||||
ABIVersion = CoreVersion
|
||||
// ABIVersionMin 是兼容的最低 ABI 标识版本。
|
||||
ABIVersionMin = "0.8.0"
|
||||
)
|
||||
|
||||
const (
|
||||
ABIVersion = 1
|
||||
ABIVersionMin = 1
|
||||
// CABINum 是 C 层协商用的整数版本(major*100 + minor),随 ABIVersion 派生。
|
||||
CABINum = 900
|
||||
// CABINumMin 是 C 层兼容的最低整数版本。
|
||||
CABINumMin = 800
|
||||
)
|
||||
|
||||
// ---- Dispatch Method IDs(与核心仓 internal/meta/meta.go 同步) ----
|
||||
|
||||
@ -113,7 +113,7 @@ func cmdInit(args []string) {
|
||||
Targets: targets,
|
||||
},
|
||||
IsLua: isLua,
|
||||
CABIVersion: meta.ABIVersion,
|
||||
CABIVersion: meta.CABINum,
|
||||
CABIHeader: tmplCABIHeader,
|
||||
}
|
||||
|
||||
|
||||
@ -330,8 +330,8 @@ func main() {}
|
||||
const tmplCABIHeader = `
|
||||
#ifndef HOMEAGENT_CABI_H
|
||||
#define HOMEAGENT_CABI_H
|
||||
// HOMEAGENT_ABI_VERSION 与 sdk/meta/meta.go ABIVersion 同步
|
||||
#define HOMEAGENT_ABI_VERSION 1
|
||||
// HOMEAGENT_ABI_VERSION 与 sdk/meta/meta.go CABINum 同步(major*100+minor,v0.9.x→900)
|
||||
#define HOMEAGENT_ABI_VERSION 900
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
@ -343,7 +343,7 @@ typedef struct {
|
||||
int (*start_plugin)(void*, int, char**);
|
||||
int (*stop_plugin)(char**);
|
||||
int (*invoke_tool)(char*, char*, char**, char**);
|
||||
int (*invoke_stage)(char*, char*, char**);
|
||||
int (*invoke_stage)(char*, char*, char**, char**);
|
||||
int (*invoke_output)(char*, char*, char*, char**);
|
||||
void (*free_string)(char*);
|
||||
} PluginAPI;
|
||||
@ -684,8 +684,53 @@ func go_invoke_tool(name *C.char, argsJSON *C.char, resultOut **C.char, errorOut
|
||||
return 0
|
||||
}
|
||||
|
||||
// fillStageContext 将内核传来的 ctx JSON 填充到插件侧 StageContext。
|
||||
func fillStageContext(sc *sdk.StageContext, ctxJSON string) {
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(ctxJSON), &m); err != nil {
|
||||
return
|
||||
}
|
||||
if v, _ := m["raw_message"].(string); v != "" { sc.RawMessage = v }
|
||||
if v, _ := m["user_id"].(string); v != "" { sc.UserID = v }
|
||||
if v, _ := m["group_id"].(string); v != "" { sc.GroupID = v }
|
||||
if v, _ := m["phase"].(string); v != "" { sc.Phase = sdk.Stage(v) }
|
||||
if v, _ := m["llm_text"].(string); v != "" { sc.LLMText = v }
|
||||
if v, _ := m["final_text"].(string); v != "" { sc.FinalText = v }
|
||||
if v, _ := m["no_memory"].(bool); v { sc.NoMemory = true }
|
||||
if v, _ := m["response"].(string); v != "" { sc.Response = &v }
|
||||
if v, _ := m["tool_calls"].([]interface{}); len(v) > 0 {
|
||||
b, _ := json.Marshal(v); json.Unmarshal(b, &sc.ToolCalls)
|
||||
}
|
||||
if v, _ := m["tool_results"].([]interface{}); len(v) > 0 {
|
||||
b, _ := json.Marshal(v); json.Unmarshal(b, &sc.ToolResults)
|
||||
}
|
||||
}
|
||||
|
||||
// stageContextWritable 提取插件可写且内核会同步回去的字段。
|
||||
func stageContextWritable(sc *sdk.StageContext) map[string]interface{} {
|
||||
m := map[string]interface{}{
|
||||
"raw_message": sc.RawMessage,
|
||||
"user_id": sc.UserID,
|
||||
"group_id": sc.GroupID,
|
||||
"phase": string(sc.Phase),
|
||||
"llm_text": sc.LLMText,
|
||||
"final_text": sc.FinalText,
|
||||
"no_memory": sc.NoMemory,
|
||||
}
|
||||
if sc.Response != nil {
|
||||
m["response"] = *sc.Response
|
||||
}
|
||||
if len(sc.ToolCalls) > 0 {
|
||||
m["tool_calls"] = sc.ToolCalls
|
||||
}
|
||||
if len(sc.ToolResults) > 0 {
|
||||
m["tool_results"] = sc.ToolResults
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
//export go_invoke_stage
|
||||
func go_invoke_stage(stage *C.char, ctxJSON *C.char, errorOut **C.char) C.int {
|
||||
func go_invoke_stage(stage *C.char, ctxJSON *C.char, resultOut **C.char, errorOut **C.char) C.int {
|
||||
goStage := C.GoString(stage)
|
||||
handlerMu.RLock()
|
||||
h, ok := stageHandlers[goStage]
|
||||
@ -693,25 +738,15 @@ func go_invoke_stage(stage *C.char, ctxJSON *C.char, errorOut **C.char) C.int {
|
||||
if !ok { return 0 }
|
||||
sc := &sdk.StageContext{}
|
||||
if ctxJSON != nil {
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(C.GoString(ctxJSON)), &m); err == nil {
|
||||
if v, _ := m["raw_message"].(string); v != "" { sc.RawMessage = v }
|
||||
if v, _ := m["user_id"].(string); v != "" { sc.UserID = v }
|
||||
if v, _ := m["group_id"].(string); v != "" { sc.GroupID = v }
|
||||
if v, _ := m["phase"].(string); v != "" { sc.Phase = sdk.Stage(v) }
|
||||
if v, _ := m["llm_text"].(string); v != "" { sc.LLMText = v }
|
||||
if v, _ := m["final_text"].(string); v != "" { sc.FinalText = v }
|
||||
if v, _ := m["no_memory"].(bool); v { sc.NoMemory = true }
|
||||
if v, _ := m["response"].(string); v != "" { sc.Response = &v }
|
||||
if v, _ := m["tool_calls"].([]interface{}); len(v) > 0 {
|
||||
b, _ := json.Marshal(v); json.Unmarshal(b, &sc.ToolCalls)
|
||||
}
|
||||
if v, _ := m["tool_results"].([]interface{}); len(v) > 0 {
|
||||
b, _ := json.Marshal(v); json.Unmarshal(b, &sc.ToolResults)
|
||||
}
|
||||
}
|
||||
fillStageContext(sc, C.GoString(ctxJSON))
|
||||
}
|
||||
if err := h(sc); err != nil { *errorOut = C.CString(err.Error()); return 1 }
|
||||
// ABI v2: 回传插件修改后的上下文(若调用方要求)
|
||||
if resultOut != nil {
|
||||
if b, err := json.Marshal(stageContextWritable(sc)); err == nil {
|
||||
*resultOut = C.CString(string(b))
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
@ -742,7 +777,8 @@ func main() {}
|
||||
const tmplPluginInitC = `#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define HOMEAGENT_ABI_VERSION 1
|
||||
// HOMEAGENT_ABI_VERSION 与 sdk/meta/meta.go CABINum 同步(major*100+minor,v0.9.x→900)
|
||||
#define HOMEAGENT_ABI_VERSION 900
|
||||
|
||||
typedef struct {
|
||||
int version; int version_min;
|
||||
@ -750,7 +786,7 @@ typedef struct {
|
||||
int (*start_plugin)(void*, int, char**);
|
||||
int (*stop_plugin)(char**);
|
||||
int (*invoke_tool)(char*, char*, char**, char**);
|
||||
int (*invoke_stage)(char*, char*, char**);
|
||||
int (*invoke_stage)(char*, char*, char**, char**);
|
||||
int (*invoke_output)(char*, char*, char*, char**);
|
||||
void (*free_string)(char*);
|
||||
} PluginAPI;
|
||||
@ -765,7 +801,7 @@ extern int go_init_plugin(char*, char*, char**);
|
||||
extern int go_start_plugin(void*, int, char**);
|
||||
extern int go_stop_plugin(char**);
|
||||
extern int go_invoke_tool(char*, char*, char**, char**);
|
||||
extern int go_invoke_stage(char*, char*, char**);
|
||||
extern int go_invoke_stage(char*, char*, char**, char**);
|
||||
extern int go_invoke_output(char*, char*, char*, char**);
|
||||
extern void go_free_string(char*);
|
||||
|
||||
@ -773,7 +809,7 @@ int c_init_plugin(char* n, char* c, char** e) { return go_init_plugin(n, c, e);
|
||||
int c_start_plugin(void* a, int v, char** e) { return go_start_plugin(a, v, e); }
|
||||
int c_stop_plugin(char** e) { return go_stop_plugin(e); }
|
||||
int c_invoke_tool(char* n, char* a, char** r, char** e) { return go_invoke_tool(n, a, r, e); }
|
||||
int c_invoke_stage(char* s, char* c, char** e) { return go_invoke_stage(s, c, e); }
|
||||
int c_invoke_stage(char* s, char* c, char** r, char** e) { return go_invoke_stage(s, c, r, e); }
|
||||
int c_invoke_output(char* c, char* m, char* p, char** e) { return go_invoke_output(c, m, p, e); }
|
||||
void c_free_string(char* p) { go_free_string(p); }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user