From 2e910e91ef8aceeca518ce966fbf592b761b68dd Mon Sep 17 00:00:00 2001 From: Virgil Date: Fri, 3 Apr 2026 19:13:21 +0000 Subject: [PATCH] feat(scope): add scoped allocation lifecycle API Co-Authored-By: Virgil --- scope.go | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++ scope_test.go | 39 ++++++++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 scope.go create mode 100644 scope_test.go diff --git a/scope.go b/scope.go new file mode 100644 index 0000000..c7f9f60 --- /dev/null +++ b/scope.go @@ -0,0 +1,82 @@ +package cgo + +/* +#include +*/ +import "C" + +import ( + "sync" + "sync/atomic" + "unsafe" +) + +// Scope tracks multiple allocations and releases them together. +// +// scope := cgo.NewScope() +// defer scope.FreeAll() +// buffer := scope.Buffer(32) +// cString := scope.CString("hello") +type Scope struct { + lock sync.Mutex + buffers []*Buffer + strings []unsafe.Pointer + freed atomic.Bool +} + +// NewScope creates a zero-value scope for grouped CGo allocations. +func NewScope() *Scope { + return &Scope{} +} + +// Buffer allocates a new managed buffer. +func (s *Scope) Buffer(size int) *Buffer { + s.lock.Lock() + defer s.lock.Unlock() + + if s.freed.Load() { + panic("cgo.Scope.Buffer: scope is already freed") + } + + buffer := NewBuffer(size) + s.buffers = append(s.buffers, buffer) + return buffer +} + +// CString allocates a managed C string. +func (s *Scope) CString(value string) *C.char { + s.lock.Lock() + defer s.lock.Unlock() + + if s.freed.Load() { + panic("cgo.Scope.CString: scope is already freed") + } + + cString := CString(value) + s.strings = append(s.strings, unsafe.Pointer(cString)) + return cString +} + +// FreeAll releases every allocation created under this scope. +func (s *Scope) FreeAll() { + if !s.freed.CompareAndSwap(false, true) { + return + } + + s.lock.Lock() + buffers := s.buffers + strings := s.strings + s.buffers = nil + s.strings = nil + s.lock.Unlock() + + for _, buffer := range buffers { + if buffer != nil && !buffer.IsFreed() { + buffer.Free() + } + } + + for _, pointer := range strings { + Free(pointer) + } +} diff --git a/scope_test.go b/scope_test.go new file mode 100644 index 0000000..d0b1c63 --- /dev/null +++ b/scope_test.go @@ -0,0 +1,39 @@ +package cgo + +import "testing" + +func TestScopeFreesAllResources(t *testing.T) { + t.Parallel() + + scope := NewScope() + buffer := scope.Buffer(4) + cString := scope.CString("agent") + if cString == nil { + t.Fatal("expected CString allocation") + } + + if copied := buffer.CopyFrom([]byte("go")); copied != 2 { + t.Fatalf("expected 2 bytes copied, got %d", copied) + } + + scope.FreeAll() + scope.FreeAll() // idempotent + + if !buffer.IsFreed() { + t.Fatal("expected buffer to be freed") + } +} + +func TestScopePanicsAfterFreeAll(t *testing.T) { + t.Parallel() + + scope := NewScope() + scope.FreeAll() + + assertPanics(t, "scope is already freed", func() { + scope.Buffer(1) + }) + assertPanics(t, "scope is already freed", func() { + scope.CString("nope") + }) +} -- 2.45.3