diff --git a/entrypoint.sh b/entrypoint.sh index 6d18e90..b8ff44b 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -148,6 +148,7 @@ test_go_ceph() { pkgs=(\ "cephfs" \ "errutil" \ + "internal/callbacks" \ "rados" \ "rbd" \ ) diff --git a/internal/callbacks/callbacks.go b/internal/callbacks/callbacks.go new file mode 100644 index 0000000..2bce42a --- /dev/null +++ b/internal/callbacks/callbacks.go @@ -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] +} diff --git a/internal/callbacks/callbacks_test.go b/internal/callbacks/callbacks_test.go new file mode 100644 index 0000000..1adffa8 --- /dev/null +++ b/internal/callbacks/callbacks_test.go @@ -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") + } + } +}