Golang中如何从函数返回Wasm指针(GC相关)
Golang中如何从函数返回Wasm指针(GC相关) 如何在不将底层结构体传递给JavaScript(将其转换为JS值)的情况下,暴露一个接受并返回(结构体)指针的Go API?
背景:我有一个对树结构(遍历、修改)进行操作的Go库。该库中的函数仅接受并返回指向这些结构体的指针。如何将这样的API暴露给JS?JS不需要访问实际的结构体,它只需要在暴露的函数之间传递引用;所有操作都由Go在wasm实例中完成。暴露API的使用示例:
const t = Tree() // wasm实例中结构体的指针
Traverse(t, function(n) {
// 遍历/修改节点
})
我目前的解决方案是使用unsafe.Pointer类型将指针传递给JS并传回。但由于Go无法在JS中跟踪这些指针,垃圾回收会回收内存。我通过创建树直到触发GC以及使用runtime.GC()测试了这一点。为了防止GC回收“存活”的内存,我创建了一个全局切片,在其中存储我不希望被垃圾回收的结构体指针。通过之前相同的方式进行测试,这似乎可行,但我不喜欢这种方法,也不知道它是否真正安全或正确。
更多关于Golang中如何从函数返回Wasm指针(GC相关)的实战教程也可以访问 https://www.itying.com/category-94-b0.html
你能分享一下你的解决方案的代码片段吗?
更多关于Golang中如何从函数返回Wasm指针(GC相关)的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
在Go WebAssembly中,暴露结构体指针给JavaScript时确实需要特殊处理来避免垃圾回收问题。以下是几种解决方案:
1. 使用uintptr作为句柄(推荐)
package main
import (
"syscall/js"
"unsafe"
)
type Tree struct {
// 树结构定义
data []int
left *Tree
right *Tree
}
var (
trees = make(map[uintptr]*Tree)
nextID uintptr = 1
)
//export Tree
func Tree() js.Value {
t := &Tree{}
id := nextID
trees[id] = t
nextID++
return js.ValueOf(float64(id))
}
//export Traverse
func Traverse(treeID js.Value, callback js.Value) {
id := uintptr(treeID.Float())
t, exists := trees[id]
if !exists {
return
}
// 遍历树的实现
traverseInternal(t, callback)
}
func traverseInternal(t *Tree, callback js.Value) {
if t == nil {
return
}
// 调用JavaScript回调
callback.Invoke(js.ValueOf(float64(uintptr(unsafe.Pointer(t)))))
traverseInternal(t.left, callback)
traverseInternal(t.right, callback)
}
//export FreeTree
func FreeTree(treeID js.Value) {
id := uintptr(treeID.Float())
delete(trees, id)
}
2. 使用Finalizer确保清理
package main
import (
"runtime"
"syscall/js"
"sync"
"unsafe"
)
type TreeHandle struct {
ID uintptr
Tree *Tree
}
var (
treeRegistry = struct {
sync.RWMutex
m map[uintptr]*TreeHandle
}{m: make(map[uintptr]*TreeHandle)}
)
//export CreateTree
func CreateTree() js.Value {
t := &Tree{}
handle := &TreeHandle{
ID: uintptr(unsafe.Pointer(t)),
Tree: t,
}
treeRegistry.Lock()
treeRegistry.m[handle.ID] = handle
treeRegistry.Unlock()
// 设置finalizer,当handle不再被引用时清理
runtime.SetFinalizer(handle, func(h *TreeHandle) {
treeRegistry.Lock()
delete(treeRegistry.m, h.ID)
treeRegistry.Unlock()
})
return js.ValueOf(float64(handle.ID))
}
//export ProcessTree
func ProcessTree(treeID js.Value) js.Value {
id := uintptr(treeID.Float())
treeRegistry.RLock()
handle, exists := treeRegistry.m[id]
treeRegistry.RUnlock()
if !exists {
return js.ValueOf(0)
}
// 处理树
result := process(handle.Tree)
return js.ValueOf(float64(result))
}
3. 使用弱引用和显式生命周期管理
package main
import (
"sync"
"syscall/js"
"unsafe"
)
type TreeManager struct {
mu sync.RWMutex
trees map[uintptr]*Tree
refs map[uintptr]int // 引用计数
}
func NewTreeManager() *TreeManager {
return &TreeManager{
trees: make(map[uintptr]*Tree),
refs: make(map[uintptr]int),
}
}
func (tm *TreeManager) NewTree() uintptr {
t := &Tree{}
ptr := uintptr(unsafe.Pointer(t))
tm.mu.Lock()
tm.trees[ptr] = t
tm.refs[ptr] = 1
tm.mu.Unlock()
return ptr
}
func (tm *TreeManager) GetTree(ptr uintptr) *Tree {
tm.mu.RLock()
defer tm.mu.RUnlock()
return tm.trees[ptr]
}
func (tm *TreeManager) AddRef(ptr uintptr) {
tm.mu.Lock()
tm.refs[ptr]++
tm.mu.Unlock()
}
func (tm *TreeManager) Release(ptr uintptr) {
tm.mu.Lock()
tm.refs[ptr]--
if tm.refs[ptr] == 0 {
delete(tm.trees, ptr)
delete(tm.refs, ptr)
}
tm.mu.Unlock()
}
// 导出给JavaScript的函数
//export CreateTreeWithManager
func CreateTreeWithManager() js.Value {
tm := getGlobalTreeManager()
ptr := tm.NewTree()
return js.ValueOf(float64(ptr))
}
//export UseTree
func UseTree(treePtr js.Value) {
ptr := uintptr(treePtr.Float())
tm := getGlobalTreeManager()
tm.AddRef(ptr)
// 使用树
tree := tm.GetTree(ptr)
if tree != nil {
// 执行操作
}
// 完成后释放
tm.Release(ptr)
}
4. 使用JavaScript对象包装(更安全)
package main
import "syscall/js"
type TreeWrapper struct {
tree *Tree
}
//export CreateTreeWrapper
func CreateTreeWrapper() js.Value {
wrapper := &TreeWrapper{
tree: &Tree{},
}
// 创建JavaScript对象
obj := js.Global().Get("Object").New()
obj.Set("_ptr", js.ValueOf(wrapper))
// 添加方法
obj.Set("traverse", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
ptr := this.Get("_ptr")
if !ptr.Truthy() {
return nil
}
wrapper := ptr.Interface().(*TreeWrapper)
callback := args[0]
// 实现遍历
traverseWithCallback(wrapper.tree, callback)
return nil
}))
// 添加析构函数
obj.Set("free", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
this.Set("_ptr", js.Undefined())
return nil
}))
return obj
}
func traverseWithCallback(t *Tree, callback js.Value) {
if t == nil {
return
}
// 创建节点对象给JavaScript
nodeObj := js.Global().Get("Object").New()
// 设置节点属性...
callback.Invoke(nodeObj)
traverseWithCallback(t.left, callback)
traverseWithCallback(t.right, callback)
}
JavaScript使用示例
// 使用方法1
const treeId = wasm.exports.Tree();
wasm.exports.Traverse(treeId, (nodeId) => {
// 处理节点
});
// 使用方法4
const tree = wasm.exports.CreateTreeWrapper();
tree.traverse((node) => {
// 修改节点
node.value = 100;
});
tree.free(); // 显式释放
关键点:
- 使用
uintptr作为不透明的句柄传递给JavaScript - 在Go端维护注册表来防止垃圾回收
- 提供显式的清理函数供JavaScript调用
- 考虑引用计数管理复杂对象的生命周期
第一种方法是最直接且常用的解决方案,通过维护一个全局的映射来确保对象在需要时保持存活状态。

