mirror of https://github.com/ceph/go-ceph
callbacks: create a helper type for managing callbacks between C and Go
As per the Go Wiki page on cgo [1] passing callback with data between C and Go is non-trivial due to the pointer passing rules [2]. This change creates a new helper type, based on the examples on the wiki, that uses opaque indexes to exchange between C and Go. For now, this type is simple as we're only going to have a few callbacks initially but this could easily be improved and made more general in the future as needed. Because we anticipate having to use this type in all of our core packages (rados and cephfs as well as rbd) we are creating the type in an "internal" package so that we don't treat the new type as public despite the capitalized method names. [1]: https://github.com/golang/go/wiki/cgo [2]: https://golang.org/cmd/cgo/#hdr-Passing_pointers Signed-off-by: John Mulligan <jmulligan@redhat.com>
This commit is contained in:
parent
d0943906f7
commit
b3f7c6af15
|
@ -148,6 +148,7 @@ test_go_ceph() {
|
|||
pkgs=(\
|
||||
"cephfs" \
|
||||
"errutil" \
|
||||
"internal/callbacks" \
|
||||
"rados" \
|
||||
"rbd" \
|
||||
)
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
package callbacks
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// The logic of this file is largely adapted from:
|
||||
// https://github.com/golang/go/wiki/cgo#function-variables
|
||||
//
|
||||
// Also helpful:
|
||||
// https://eli.thegreenplace.net/2019/passing-callbacks-and-pointers-to-cgo/
|
||||
|
||||
// Callbacks provides a tracker for data that is to be passed between Go
|
||||
// and C callback functions. The Go callback/object may not be passed
|
||||
// by a pointer to C code and so instead integer indexes into an internal
|
||||
// map are used.
|
||||
// Typically the item being added will either be a callback function or
|
||||
// a data structure containing a callback function. It is up to the caller
|
||||
// to control and validate what "callbacks" get used.
|
||||
type Callbacks struct {
|
||||
mutex sync.RWMutex
|
||||
cmap map[int]interface{}
|
||||
}
|
||||
|
||||
// New returns a new callbacks tracker.
|
||||
func New() *Callbacks {
|
||||
return &Callbacks{cmap: make(map[int]interface{})}
|
||||
}
|
||||
|
||||
// Add a callback/object to the tracker and return a new index
|
||||
// for the object.
|
||||
func (cb *Callbacks) Add(v interface{}) int {
|
||||
cb.mutex.Lock()
|
||||
defer cb.mutex.Unlock()
|
||||
// this approach assumes that there are typically very few callbacks
|
||||
// in play at once and can just use the length of the map as our
|
||||
// index. But in case of collisions we fall back to simply incrementing
|
||||
// until we find a free key like in the cgo wiki page.
|
||||
// If this code ever becomes a hot path there's surely plenty of room
|
||||
// for optimization in the future :-)
|
||||
index := len(cb.cmap) + 1
|
||||
for {
|
||||
if _, found := cb.cmap[index]; !found {
|
||||
break
|
||||
}
|
||||
index++
|
||||
}
|
||||
cb.cmap[index] = v
|
||||
return index
|
||||
}
|
||||
|
||||
// Remove a callback/object given it's index.
|
||||
func (cb *Callbacks) Remove(index int) {
|
||||
cb.mutex.Lock()
|
||||
defer cb.mutex.Unlock()
|
||||
delete(cb.cmap, index)
|
||||
}
|
||||
|
||||
// Lookup returns a mapped callback/object given an index.
|
||||
func (cb *Callbacks) Lookup(index int) interface{} {
|
||||
cb.mutex.RLock()
|
||||
defer cb.mutex.RUnlock()
|
||||
return cb.cmap[index]
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
package callbacks
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCallbacks(t *testing.T) {
|
||||
cbks := New()
|
||||
assert.Len(t, cbks.cmap, 0)
|
||||
|
||||
i1 := cbks.Add("foo")
|
||||
i2 := cbks.Add("bar")
|
||||
i3 := cbks.Add("baz")
|
||||
assert.Len(t, cbks.cmap, 3)
|
||||
|
||||
var x interface{}
|
||||
x = cbks.Lookup(i1)
|
||||
assert.NotNil(t, x)
|
||||
if s, ok := x.(string); ok {
|
||||
assert.EqualValues(t, s, "foo")
|
||||
}
|
||||
|
||||
x = cbks.Lookup(5555)
|
||||
assert.Nil(t, x)
|
||||
|
||||
x = cbks.Lookup(i3)
|
||||
assert.NotNil(t, x)
|
||||
if s, ok := x.(string); ok {
|
||||
assert.EqualValues(t, s, "baz")
|
||||
}
|
||||
cbks.Remove(i3)
|
||||
x = cbks.Lookup(i3)
|
||||
assert.Nil(t, x)
|
||||
|
||||
cbks.Remove(i2)
|
||||
x = cbks.Lookup(i2)
|
||||
assert.Nil(t, x)
|
||||
|
||||
cbks.Remove(i1)
|
||||
assert.Len(t, cbks.cmap, 0)
|
||||
}
|
||||
|
||||
func TestCallbacksIndexing(t *testing.T) {
|
||||
cbks := New()
|
||||
assert.Len(t, cbks.cmap, 0)
|
||||
|
||||
i1 := cbks.Add("foo")
|
||||
i2 := cbks.Add("bar")
|
||||
_ = cbks.Add("baz")
|
||||
_ = cbks.Add("wibble")
|
||||
_ = cbks.Add("wabble")
|
||||
assert.Len(t, cbks.cmap, 5)
|
||||
|
||||
// generally we assume that the callback data will be mostly LIFO
|
||||
// but can't guarantee it. Thus we check that when we remove the
|
||||
// first items inserted into the map there are no subsequent issues
|
||||
cbks.Remove(i1)
|
||||
cbks.Remove(i2)
|
||||
_ = cbks.Add("flim")
|
||||
ilast := cbks.Add("flam")
|
||||
assert.Len(t, cbks.cmap, 5)
|
||||
|
||||
x := cbks.Lookup(ilast)
|
||||
assert.NotNil(t, x)
|
||||
if s, ok := x.(string); ok {
|
||||
assert.EqualValues(t, s, "flam")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCallbacksData(t *testing.T) {
|
||||
cbks := New()
|
||||
assert.Len(t, cbks.cmap, 0)
|
||||
|
||||
// insert a plain function
|
||||
i1 := cbks.Add(func(v int) int { return v + 1 })
|
||||
|
||||
// insert a type "containing" a function, note that it doesn't
|
||||
// actually have a callable function. Users of the type must
|
||||
// check that themselves
|
||||
type flup struct {
|
||||
Stuff int
|
||||
Junk func(int, int) error
|
||||
}
|
||||
i2 := cbks.Add(flup{
|
||||
Stuff: 55,
|
||||
})
|
||||
|
||||
// did we get a function back
|
||||
x1 := cbks.Lookup(i1)
|
||||
if assert.NotNil(t, x1) {
|
||||
if f, ok := x1.(func(v int) int); ok {
|
||||
assert.Equal(t, 2, f(1))
|
||||
} else {
|
||||
t.Fatalf("conversion failed")
|
||||
}
|
||||
}
|
||||
|
||||
// did we get our data structure back
|
||||
x2 := cbks.Lookup(i2)
|
||||
if assert.NotNil(t, x2) {
|
||||
if d, ok := x2.(flup); ok {
|
||||
assert.Equal(t, 55, d.Stuff)
|
||||
assert.Nil(t, d.Junk)
|
||||
} else {
|
||||
t.Fatalf("conversion failed")
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue