是否可以 export/wrap 一个复杂的 Go 结构到 C?

Is it possible to export/wrap a complex Go struct to C?

我拥有一个 Go 库 gofileseq,我想尝试为它创建一个 C/C++ 绑定。

能够导出使用简单类型(整数、字符串...)的函数非常简单。通过定义 C 结构并将 Go 类型转换为它,将数据从自定义 Go 类型导出到 C 甚至很容易,以便在导出的函数中使用,因为您正在分配 C 内存来执行此操作。但是对于 go 1.5 cgo rules,我发现很难弄清楚如何从存储状态的更复杂的结构中导出功能。

我想以某种方式导出到 C++ 绑定的来自 gofileseq 的结构示例:

// package fileseq
//

type FrameSet struct {
    frange   string
    rangePtr *ranges.InclusiveRanges
}

func NewFrameSet(frange string) (*FrameSet, error) {
    // bunch of processing to set up internal state
}

func (s *FrameSet) Len() int {
    return s.rangePtr.Len()
}

// package ranges
//

type InclusiveRanges struct {
    blocks []*InclusiveRange
}

type InclusiveRange struct {
    start int
    end   int
    step  int

    cachedEnd   int
    isEndCached bool

    cachedLen   int
    isLenCached bool
}

如您所见,我要公开的 FrameSet 类型包含指向基础类型的指针切片,每个指针都存储状态。

理想情况下,我希望能够在 C++ class 上存储一个 void*,并使其成为一个简单的代理,用于使用 [=13= 回调导出的 Go 函数].但是 cgo 规则不允许 C 存储比函数调用更长的 Go 指针。而且我看不出如何使用定义 C++ classes 的方法,这些方法可以分配并用于与我的 Go 库一起操作。

是否可以包装复杂类型以暴露给 C/C++? 是否存在允许 C++ 客户端创建 Go FrameSet 的模式?

编辑

我能想到的一个想法是让 C++ 在 Go 中创建对象,这些对象以静态 map[int]*FrameSet 的形式存储在 Go 端,然后 return 将 int id 存储到 C++。然后,所有 C++ 操作都使用 id 向 Go 发出请求。这听起来像是一个有效的解决方案吗?

更新

目前,我正在测试一个使用全局地图和唯一 ID 来存储对象的解决方案。 C++ 会请求创建一个新对象,但只会返回一个不透明的 ID。然后他们可以调用所有导出为函数的方法,使用该 id,包括请求在完成时将其销毁。

如果有比这更好的方法,我很乐意看到答案。一旦我得到一个完整的工作原型,我将添加我自己的答案。

更新 #2

我写了一篇博客 post 关于我最终使用的最终解决方案:http://justinfx.com/2016/05/14/cpp-bindings-for-go/

由于没有更好的解决方案,我最终解决这个问题的方法是在 Go 端使用私有全局地图 (ref)。这些映射会将 Go 对象的实例与随机 uint64 id 相关联,并且该 id 将作为 "opaque handle" 返回给 C++。

type frameSetMap struct {
    lock *sync.RWMutex
    m    map[FrameSetId]*frameSetRef
    rand idMaker
}
//...
func (m *frameSetMap) Add(fset fileseq.FrameSet) FrameSetId {
    // fmt.Printf("frameset Add %v as %v\n", fset.String(), id)
    m.lock.Lock()
    id := FrameSetId(m.rand.Uint64())
    m.m[id] = &frameSetRef{fset, 1}
    m.lock.Unlock()
    return id
}

然后我使用引用计数来确定 C++ 何时不再需要该对象,并将其从映射中删除:

// Go
func (m *frameSetMap) Incref(id FrameSetId) {
    m.lock.RLock()
    ref, ok := m.m[id]
    m.lock.RUnlock()

    if !ok {
        return
    }

    atomic.AddUint32(&ref.refs, 1)
    // fmt.Printf("Incref %v to %d\n", ref, refs)
}

func (m *frameSetMap) Decref(id FrameSetId) {
    m.lock.RLock()
    ref, ok := m.m[id]
    m.lock.RUnlock()

    if !ok {
        return
    }

    refs := atomic.AddUint32(&ref.refs, ^uint32(0))
    // fmt.Printf("Decref %v to %d\n", ref, refs)
    if refs != 0 {
        return
    }

    m.lock.Lock()
    if atomic.LoadUint32(&ref.refs) == 0 {
        // fmt.Printf("Deleting %v\n", ref)
        delete(m.m, id)
    }
    m.lock.Unlock()
}

//C++
FileSequence::~FileSequence() {
    if (m_valid) {
//        std::cout << "FileSequence destroy " << m_id << std::endl;
        m_valid = false;
        internal::FileSequence_Decref(m_id);
        m_id = 0;
        m_fsetId = 0;
    }
}

所有与导出的 Go 库的 C++ 交互都通过不透明句柄进行通信:

// C++
size_t FileSequence::length() const {
    return internal::FileSequence_Len(m_id);
}

不幸的是,这确实意味着在多线程 C++ 环境中,所有线程都将通过互斥锁到达映射。但是它只是在创建和销毁对象时是写锁,对于对象上的所有方法调用都是读锁。