是否可以将复杂的Go结构导出/包装到C?

时间:2016-04-26 09:59:47

标签: c++ c go cgo

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

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

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

// 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 ++类上存储void*,并使其成为使用void*回调导出的Go函数的简单代理。但是cgo规则禁止C存储Go指针比函数调用更长。而且我没有看到如何使用一种方法来定义可以分配并用于与Go库一起操作的C ++类。

是否可以将复杂类型包装为C / C ++? 是否有一种模式允许C ++客户端创建Go FrameSet

修改

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

更新

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

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

更新#2

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

1 个答案:

答案 0 :(得分:0)

由于缺乏更好的解决方案,我最终解决这个问题的方法是在Go端使用私有全局地图(ref)。这些映射会将Go对象的实例与随机的uint64 id相关联,并且id将作为" opaque句柄返回到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 ++交互都通过opaque句柄进行通信:

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

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