diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b3cc4c1 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/mattn/go-pointer + +go 1.14 + +require github.com/stretchr/testify v1.6.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..56d62e7 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pointer.go b/pointer.go index 08a9853..5673d1d 100644 --- a/pointer.go +++ b/pointer.go @@ -2,37 +2,63 @@ package pointer // #include import "C" + import ( "sync" "unsafe" ) +const blockSize = 1024 + var ( - mutex sync.RWMutex - store = map[unsafe.Pointer]interface{}{} + mutex sync.RWMutex + store = map[unsafe.Pointer]interface{}{} + free []unsafe.Pointer + blocks []unsafe.Pointer ) -func Save(v interface{}) unsafe.Pointer { - if v == nil { - return nil +func allocMem() { + mem := C.malloc(blockSize) + if mem == nil { + panic("can't allocate memory block for C pointers") } + blocks = append(blocks, mem) + for i := 0; i < blockSize; i++ { + p := unsafe.Pointer(uintptr(mem) + uintptr(blockSize-1-i)) + free = append(free, p) + } +} +func getPtr() unsafe.Pointer { // Generate real fake C pointer. - // This pointer will not store any data, but will bi used for indexing purposes. - // Since Go doest allow to cast dangling pointer to unsafe.Pointer, we do rally allocate one byte. - // Why we need indexing, because Go doest allow C code to store pointers to Go data. - var ptr unsafe.Pointer = C.malloc(C.size_t(1)) - if ptr == nil { - panic("can't allocate 'cgo-pointer hack index pointer': ptr == nil") + // This pointer will not store any data, but will be used for indexing + // purposes. Since Go doesn't allow to cast dangling pointer to + // unsafe.Pointer, we do really allocate memory. Why we need indexing? Because + // Go doest allow C code to store pointers to Go data. + if len(free) == 0 { + allocMem() + } + n := len(free) - 1 + p := free[n] + free = free[:n] + return p +} + +// Save an object in the storage and return an index pointer to it. +func Save(v interface{}) unsafe.Pointer { + if v == nil { + return nil } mutex.Lock() + ptr := getPtr() store[ptr] = v mutex.Unlock() return ptr } +// Restore an object from the storage by its index pointer. func Restore(ptr unsafe.Pointer) (v interface{}) { if ptr == nil { return nil @@ -44,14 +70,31 @@ func Restore(ptr unsafe.Pointer) (v interface{}) { return } +// Unref removes an object from the storage and returns the index pointer to the +// pool for reuse. func Unref(ptr unsafe.Pointer) { if ptr == nil { return } mutex.Lock() - delete(store, ptr) + if _, ok := store[ptr]; ok { + delete(store, ptr) + free = append(free, ptr) + } mutex.Unlock() +} - C.free(ptr) +// Clear storage and free all memory +func Clear() { + mutex.Lock() + for p := range store { + delete(store, p) + } + free = nil + for _, p := range blocks { + C.free(p) + } + blocks = nil + mutex.Unlock() } diff --git a/pointer_test.go b/pointer_test.go new file mode 100644 index 0000000..a3ef460 --- /dev/null +++ b/pointer_test.go @@ -0,0 +1,176 @@ +package pointer + +import ( + "sync" + "testing" + "unsafe" + + "github.com/stretchr/testify/assert" +) + +func TestPointer(t *testing.T) { + t.Cleanup(Clear) + // assert := makeAssert(t) + assert.Len(t, store, 0) + assert.Len(t, free, 0) + assert.Len(t, blocks, 0) + mutex.Lock() + assert.Equal(t, unsafe.Pointer(nil), Save(nil)) + assert.Nil(t, Restore(nil)) + Unref(nil) + mutex.Unlock() + i1 := Save("foo") + i2 := Save("bar") + i3 := Save("baz") + assert.Len(t, store, 3) + assert.Len(t, free, blockSize-3) + assert.Len(t, blocks, 1) + var x interface{} + x = Restore(i1) + assert.NotNil(t, x) + if s, ok := x.(string); ok { + assert.Equal(t, "foo", s) + } else { + t.Fail() + } + x = Restore(unsafe.Pointer(&x)) + assert.Nil(t, x) + x = Restore(i3) + assert.NotNil(t, x) + if s, ok := x.(string); ok { + assert.Equal(t, "baz", s) + } else { + t.Fail() + } + Unref(i3) + x = Restore(i3) + assert.Nil(t, x) + Unref(i2) + Unref(i1) + assert.Len(t, store, 0) + assert.Len(t, free, blockSize) + assert.Len(t, blocks, 1) + i3 = Save("baz") + assert.Len(t, store, 1) + assert.Len(t, free, blockSize-1) + Clear() + assert.Len(t, store, 0) + assert.Len(t, free, 0) + assert.Len(t, blocks, 0) +} + +func TestPointerIndexing(t *testing.T) { + t.Cleanup(Clear) + assert.Len(t, store, 0) + assert.Len(t, free, 0) + assert.Len(t, blocks, 0) + + i1 := Save("foo") + i2 := Save("bar") + _ = Save("baz") + _ = Save("wibble") + _ = Save("wabble") + assert.Len(t, store, 5) + assert.Len(t, free, blockSize-5) + + // Check that when we remove the first items inserted into the map there are + // no subsequent issues + Unref(i1) + Unref(i2) + assert.Len(t, free, blockSize-3) + _ = Save("flim") + ilast := Save("flam") + assert.Len(t, store, 5) + assert.Len(t, free, blockSize-5) + + x := Restore(ilast) + assert.NotNil(t, x) + if s, ok := x.(string); ok { + assert.Equal(t, "flam", s) + } +} + +func TestCallbacksData(t *testing.T) { + t.Cleanup(Clear) + assert.Len(t, store, 0) + assert.Len(t, free, 0) + assert.Len(t, blocks, 0) + + // insert a plain function + i1 := Save(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 := Save(flup{ + Stuff: 55, + }) + + // did we get a function back + x1 := Restore(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 := Restore(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") + } + } +} + +func BenchmarkPointer(t *testing.B) { + t.Cleanup(Clear) + assert.Len(t, store, 0) + assert.Len(t, free, 0) + assert.Len(t, blocks, 0) + const workers = 1000 + var wg sync.WaitGroup + f := func() { + defer wg.Done() + var x interface{} + var i1, i2, i3 unsafe.Pointer + for i := 0; i < t.N/workers; i++ { + i1 = Save("foo") + i2 = Save("bar") + i3 = Save("baz") + x = Restore(i1) + assert.NotNil(t, x) + if s, ok := x.(string); ok { + assert.Equal(t, "foo", s) + } else { + t.Fail() + } + x = Restore(i3) + assert.NotNil(t, x) + if s, ok := x.(string); ok { + assert.Equal(t, "baz", s) + } else { + t.Fail() + } + Unref(i3) + Unref(i2) + Unref(i1) + } + } + for i := 0; i < workers; i++ { + wg.Add(1) + go f() + } + wg.Wait() + assert.Len(t, store, 0) + assert.Len(t, free, len(blocks)*blockSize) +}