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

2 回复

你能分享一下你的解决方案的代码片段吗?

更多关于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(); // 显式释放

关键点:

  1. 使用uintptr作为不透明的句柄传递给JavaScript
  2. 在Go端维护注册表来防止垃圾回收
  3. 提供显式的清理函数供JavaScript调用
  4. 考虑引用计数管理复杂对象的生命周期

第一种方法是最直接且常用的解决方案,通过维护一个全局的映射来确保对象在需要时保持存活状态。

回到顶部