diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..00741cb --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +.idea/ \ No newline at end of file diff --git a/assert.go b/assert.go new file mode 100644 index 0000000..6351355 --- /dev/null +++ b/assert.go @@ -0,0 +1 @@ +package xx diff --git a/assert_disabled.go b/assert_disabled.go new file mode 100644 index 0000000..6351355 --- /dev/null +++ b/assert_disabled.go @@ -0,0 +1 @@ +package xx diff --git a/mem/mem.go b/mem/mem.go new file mode 100644 index 0000000..5a89be0 --- /dev/null +++ b/mem/mem.go @@ -0,0 +1,74 @@ +package mem + +import ( + "unsafe" +) + +// SizeOf returns the size (in bytes) of the given type. +// +// Not to be confused with [unsafe.Sizeof] which returns the size of a type via an expression. +func SizeOf[T any]() uintptr { + var zero T + return unsafe.Sizeof(zero) +} + +// AlignOf returns the alignment (in bytes) of the given type. +// +// Not to be confused with [unsafe.AlignOf] which returns the alignment of a type via an expression. +func AlignOf[T any]() uintptr { + var zero T + return unsafe.Alignof(zero) +} + +// BitCast performs a bit conversion between two types of the same size. +// +// BitCast panics if the sizes of the types differ. +func BitCast[TOut any, TIn any](value *TIn) TOut { + if SizeOf[TOut]() != SizeOf[TIn]() { + panic("bitcast: sizes of types must match") + } + return *((*TOut)(unsafe.Pointer(value))) +} + +// Copy copies size number of bytes from src into dst. +// +// Returns dst. +func Copy(dst, src unsafe.Pointer, size uintptr) unsafe.Pointer { + copy(unsafe.Slice((*byte)(dst), size), unsafe.Slice((*byte)(src), size)) + return dst +} + +// Clear overwrites 'count' number of bytes in 'dst' with a particular value. +// +// Returns dst. +func Clear(dst unsafe.Pointer, value byte, count uintptr) unsafe.Pointer { + b := (*byte)(dst) + for range count { + *b = value + b = (*byte)(unsafe.Add(dst, 1)) + } + return dst +} + +// Zero overwrites 'count' number of bytes in 'dst' with zeros. +// +// Returns dst. +func Zero(dst unsafe.Pointer, count uintptr) unsafe.Pointer { + return Clear(dst, 0, count) +} + +// AlignForward returns an address align to the next power-of-two alignment. +func AlignForward(address uintptr, alignment uintptr) uintptr { + if alignment == 0 || (alignment&(alignment-1)) != 0 { + panic("alignforward: alignment must be a power of two") + } + return (address + alignment - 1) &^ (alignment - 1) +} + +// AlignBackward returns an address align to the previous power-of-two alignment. +func AlignBackward(address uintptr, alignment uintptr) uintptr { + if alignment == 0 || (alignment&(alignment-1)) != 0 { + panic("alignbackward: alignment must be a power of two") + } + return address &^ (alignment - 1) +} diff --git a/mem/mem_test.go b/mem/mem_test.go new file mode 100644 index 0000000..a645f81 --- /dev/null +++ b/mem/mem_test.go @@ -0,0 +1,26 @@ +package mem_test + +import ( + "testing" + + "git.brut.systems/judah/xx/mem" +) + +func TestBitCast(t *testing.T) { + a := uint32(0xFFFF_FFFF) + b := mem.BitCast[float32](&a) + c := mem.BitCast[uint32](&b) + if a != c { + t.Fail() + } + + v := uint8(0xFF) + d := mem.BitCast[int8](&v) + if d != -1 { + t.Fail() + } + e := mem.BitCast[uint8](&d) + if e != 255 { + t.Fail() + } +} diff --git a/stable/array.go b/stable/array.go new file mode 100644 index 0000000..503ff3a --- /dev/null +++ b/stable/array.go @@ -0,0 +1,112 @@ +package stable + +import ( + "iter" +) + +const DefaultElementsPerBucket = 32 + +// Array is a resizable array whose values will never move in memory. +// This means it is safe to take a pointer to a value within the array +// while continuing to append to it. +type Array[T any] struct { + buckets []bucket[T] + last int + elements_per_bucket int +} + +func (s *Array[T]) Init() { + s.InitWithCapacity(DefaultElementsPerBucket) +} + +func (s *Array[T]) InitWithCapacity(elements_per_bucket int) { + if elements_per_bucket <= 0 { + elements_per_bucket = DefaultElementsPerBucket + } + + s.elements_per_bucket = elements_per_bucket + + s.buckets = s.buckets[:0] + s.buckets = append(s.buckets, make(bucket[T], 0, s.elements_per_bucket)) + s.last = 0 +} + +func (s *Array[T]) Reset() { + s.buckets = s.buckets[:0] + s.last = 0 +} + +func (s *Array[T]) Append(value T) *T { + if len(s.buckets) == 0 { + s.Init() + } + + if len(s.buckets[s.last]) == cap(s.buckets[s.last]) { + s.buckets = append(s.buckets, make(bucket[T], 0, s.elements_per_bucket)) + s.last += 1 + } + + s.buckets[s.last] = append(s.buckets[s.last], value) + return &s.buckets[s.last][len(s.buckets[s.last])-1] +} + +func (s *Array[T]) AppendMany(values ...T) (first *T) { + if len(values) == 0 { + return nil + } + + first = s.Append(values[0]) + + if len(values) > 1 { + for _, v := range values[1:] { + s.Append(v) + } + } + + return +} + +func (s *Array[T]) Get(index int) *T { + b := s.buckets[index/s.elements_per_bucket] + return &b[index%s.elements_per_bucket] +} + +func (s *Array[T]) Set(index int, value T) { + *s.Get(index) = value +} + +func (s *Array[T]) Len() int { + return s.Cap() - (cap(s.buckets[s.last]) - len(s.buckets[s.last])) +} + +func (s *Array[T]) Cap() int { + return len(s.buckets) * s.elements_per_bucket +} + +func (s *Array[T]) Pointers() iter.Seq2[int, *T] { + return func(yield func(int, *T) bool) { + for bi := range s.buckets { + startIdx := bi * s.elements_per_bucket + for i := range s.buckets[bi] { + if !yield(startIdx+i, &s.buckets[bi][i]) { + return + } + } + } + } +} + +func (s *Array[T]) Values() iter.Seq2[int, T] { + return func(yield func(int, T) bool) { + for bi, b := range s.buckets { + startIdx := bi * s.elements_per_bucket + for i := range b { + if !yield(startIdx+i, b[i]) { + return + } + } + } + } +} + +type bucket[T any] = []T diff --git a/stable/array_test.go b/stable/array_test.go new file mode 100644 index 0000000..ff17a89 --- /dev/null +++ b/stable/array_test.go @@ -0,0 +1,82 @@ +package stable_test + +import ( + "runtime" + "testing" + + "git.brut.systems/judah/xx/stable" +) + +func TestArray_StableWithGC(t *testing.T) { + type valuewithptr struct { + value int + ptr *int + } + + var arr stable.Array[valuewithptr] + aptr := arr.Append(valuewithptr{value: 10, ptr: nil}) + bptr := arr.Append(valuewithptr{value: 20, ptr: &aptr.value}) + + const N = 1000 + for i := range N { + arr.Append(valuewithptr{value: i}) + runtime.GC() + } + + expect(t, arr.Get(0) == aptr) + expect(t, arr.Get(1) == bptr) + expect(t, arr.Len() == N+2, "len was %d", arr.Len()) + expect(t, bptr.ptr != nil && bptr.value == 20) + expect(t, bptr.ptr == &aptr.value, "%p vs. %p", bptr.ptr, &aptr.value) +} + +func BenchmarkArray_RandomAccess(b *testing.B) { + var arr stable.Array[int] + for i := range b.N { + arr.Append(i * i) + } + + b.ResetTimer() + for i := range b.N { + arr.Get(i % 10000) + } +} + +func BenchmarkArray_Append(b *testing.B) { + var arr stable.Array[int] + for i := range b.N { + arr.Append(i * i) + } + + arr.Reset() + for i := range b.N { + arr.Append(i * i) + } +} + +func BenchmarkArray_Iteration(b *testing.B) { + var arr stable.Array[int] + for i := range b.N { + arr.Append(i * i) + } + + b.ResetTimer() + + sum := 0 + for _, v := range arr.Values() { + sum += v + } +} + +func expect(t *testing.T, cond bool, message ...any) { + t.Helper() + + if !cond { + if len(message) == 0 { + message = append(message, "assertion failed") + } + + str := message[0].(string) + t.Fatalf(str, message[1:]...) + } +} diff --git a/stable/pointer_test.go b/stable/pointer_test.go new file mode 100644 index 0000000..5ce6b6e --- /dev/null +++ b/stable/pointer_test.go @@ -0,0 +1,117 @@ +package pointer + +import ( + "testing" +) + +func TestAlignForward(t *testing.T) { + tests := []struct { + name string + offset uintptr + alignment uintptr + }{ + {"align 8 bytes", 1, 8}, + {"align 16 bytes", 3, 16}, + {"align 32 bytes", 7, 32}, + {"align 64 bytes", 15, 64}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + value := int32(789) + pinned := Pin(&value) + defer pinned.Unpin() + + // Add offset to misalign + misaligned := pinned.Add(tt.offset) + aligned := misaligned.AlignForward(tt.alignment) + + // Check alignment + if aligned.Address()%tt.alignment != 0 { + t.Errorf("Address %d is not aligned to %d bytes", aligned.Address(), tt.alignment) + } + + // Check it's forward aligned (greater or equal) + if aligned.Address() < misaligned.Address() { + t.Errorf("Forward aligned address %d should be >= original %d", aligned.Address(), misaligned.Address()) + } + }) + } +} + +func TestAlignBackward(t *testing.T) { + tests := []struct { + name string + offset uintptr + alignment uintptr + }{ + {"align 8 bytes", 5, 8}, + {"align 16 bytes", 10, 16}, + {"align 32 bytes", 20, 32}, + {"align 64 bytes", 40, 64}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + value := int32(321) + pinned := Pin(&value) + defer pinned.Unpin() + + // Add offset to misalign + misaligned := pinned.Add(tt.offset) + aligned := misaligned.AlignBackward(tt.alignment) + + // Check alignment + if aligned.Address()%tt.alignment != 0 { + t.Errorf("Address %d is not aligned to %d bytes", aligned.Address(), tt.alignment) + } + + // Check it's backward aligned (less or equal) + if aligned.Address() > misaligned.Address() { + t.Errorf("Backward aligned address %d should be <= original %d", aligned.Address(), misaligned.Address()) + } + }) + } +} + +func TestNth(t *testing.T) { + // Test with int32 array + arr := []int32{10, 20, 30, 40, 50} + pinned := Pin(&arr[0]) + defer pinned.Unpin() + + for i := 0; i < len(arr); i++ { + value := Nth[int32](pinned, i) + if value != arr[i] { + t.Errorf("Index %d: expected %d, got %d", i, arr[i], value) + } + } +} + +func TestNthFloat64(t *testing.T) { + // Test with a float64 array + arr := []float64{1.1, 2.2, 3.3, 4.4, 5.5} + pinned := Pin(&arr[0]) + defer pinned.Unpin() + + for i := 0; i < len(arr); i++ { + value := Nth[float64](pinned, i) + if value != arr[i] { + t.Errorf("Index %d: expected %f, got %f", i, arr[i], value) + } + } +} + +func TestPointerArithmeticChain(t *testing.T) { + value := int32(888) + pinned := Pin(&value) + defer pinned.Unpin() + + // Test chaining operations + result := pinned.Add(16).Add(8).Sub(4) + expected := pinned.Address() + 16 + 8 - 4 + + if result.Address() != expected { + t.Errorf("Expected address %d, got %d", expected, result.Address()) + } +} diff --git a/stable/xar.go b/stable/xar.go new file mode 100644 index 0000000..485ffde --- /dev/null +++ b/stable/xar.go @@ -0,0 +1 @@ +package stable diff --git a/stable/xar_test.go b/stable/xar_test.go new file mode 100644 index 0000000..485ffde --- /dev/null +++ b/stable/xar_test.go @@ -0,0 +1 @@ +package stable diff --git a/utils.go b/utils.go deleted file mode 100644 index d712129..0000000 --- a/utils.go +++ /dev/null @@ -1,49 +0,0 @@ -package xx - -import ( - "unsafe" -) - -// New returns a newly allocated value with an initial value. -func New[T any](expr T) *T { - p := new(T) - *p = expr - return p -} - -// Bitcast performs a bit conversion between two types of the same size. -// -// Bitcast panics if the sizes of the types differ. -func Bitcast[TOut any, TIn any](value TIn) TOut { - if SizeOf[TOut]() != SizeOf[TIn]() { - panic("bitcast: sizes of types must match") - } - return *((*TOut)(unsafe.Pointer(&value))) -} - -// Copy copies src number of bytes into dst. -// Returns dst. -// -// Copy panics if src is smaller than dst. -func Copy[TDst any, TSrc any](dst *TDst, src *TSrc) *TDst { - if SizeOf[TSrc]() < SizeOf[TDst]() { - panic("copy: size of src must be >= dst") - } - MemCopy(unsafe.Pointer(dst), unsafe.Pointer(src), SizeOf[TDst]()) - return dst -} - -// MemCopy copies size number of bytes from src into dst. -// Returns dst. -func MemCopy(dst, src unsafe.Pointer, size uintptr) unsafe.Pointer { - copy(unsafe.Slice((*byte)(dst), size), unsafe.Slice((*byte)(src), size)) - return dst -} - -// SizeOf returns the size in bytes of the given type. -// -// Not to be confused with [unsafe.Sizeof] which returns the size of an expression. -func SizeOf[T any]() uintptr { - var zero T - return unsafe.Sizeof(zero) -} diff --git a/xx.go b/xx.go new file mode 100644 index 0000000..7bbbb05 --- /dev/null +++ b/xx.go @@ -0,0 +1,35 @@ +package xx + +import ( + "unsafe" + + "git.brut.systems/judah/xx/mem" +) + +// New returns a newly allocated value with an initial value. +func New[T any](expr T) *T { + p := new(T) + *p = expr + return p +} + +// Copy copies src number of bytes into dst. +// Returns dst. +// +// Copy panics if src is smaller than dst. +func Copy[TDst any, TSrc any](dst *TDst, src *TSrc) *TDst { + if mem.SizeOf[TSrc]() < mem.SizeOf[TDst]() { + panic("copy: size of src must be >= dst") + } + mem.Copy(unsafe.Pointer(dst), unsafe.Pointer(src), mem.SizeOf[TDst]()) + return dst +} + +// Clone returns a newly allocated shallow copy of the given value. +func Clone[T any](value *T) *T { + return Copy(new(T), value) +} + +func BoolUint(b bool) uint { + return uint(*(*uint8)(unsafe.Pointer(&b))) +} diff --git a/utils_test.go b/xx_test.go similarity index 60% rename from utils_test.go rename to xx_test.go index 7e5141c..d00d0b3 100644 --- a/utils_test.go +++ b/xx_test.go @@ -5,6 +5,7 @@ import ( "unsafe" "git.brut.systems/judah/xx" + "git.brut.systems/judah/xx/mem" ) func TestNew(t *testing.T) { @@ -13,7 +14,7 @@ func TestNew(t *testing.T) { t.Fail() } - if unsafe.Sizeof(*a) != xx.SizeOf[uint32]() { + if unsafe.Sizeof(*a) != mem.SizeOf[uint32]() { t.Fail() } @@ -42,21 +43,3 @@ func TestNew(t *testing.T) { t.Fail() } } - -func TestBitcast(t *testing.T) { - a := uint32(0xFFFF_FFFF) - b := xx.Bitcast[float32](a) - c := xx.Bitcast[uint32](b) - if a != c { - t.Fail() - } - - d := xx.Bitcast[int8](uint8(0xFF)) - if d != -1 { - t.Fail() - } - e := xx.Bitcast[uint8](d) - if e != 255 { - t.Fail() - } -}