Compare commits
No commits in common. "f83a1f323f47afa5b71ebd311cf20724ca4af6c6" and "2dd74ce66084584c347fc0556b9e89f893ac6fab" have entirely different histories.
f83a1f323f
...
2dd74ce660
6 changed files with 344 additions and 154 deletions
|
|
@ -24,7 +24,7 @@ func New[T any](arena Arena) *T {
|
|||
//
|
||||
// Note: Accessing the returned slice after calling Reset is unsafe and may result in a fault.
|
||||
func MakeSlice[T any](arena Arena, len, cap int) []T {
|
||||
ptr, err := arena(ACTION_ALLOC, mem.Sizeof[T]()*uintptr(cap), mem.Alignof[T](), nil)
|
||||
ptr, err := arena(ACTION_ALLOC, mem.Sizeof[T]()*uintptr(len), mem.Alignof[T](), nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package arena
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/bits"
|
||||
"runtime"
|
||||
|
|
@ -172,89 +171,6 @@ func Chunked(max_allocs_per_chunk uintptr) Arena {
|
|||
}
|
||||
}
|
||||
|
||||
// Paging is a linear arena that allocates pages of virtual memory.
|
||||
// The memory allocated is only committed to physical memory as it is used,
|
||||
// so total_reserved_in_bytes should is the total amount of addressable memory to reserve.
|
||||
//
|
||||
// Note: resetting a Paging arena will cause the currently commited memory to be decommited (i.e. unmapped from physical memory).
|
||||
func Paging(page_size, total_reserved_in_bytes uintptr) Arena {
|
||||
var (
|
||||
committed uintptr
|
||||
offset uintptr
|
||||
)
|
||||
|
||||
base, err := mem.Reserve(total_reserved_in_bytes)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("paging: failed to reserve address space - %s", err))
|
||||
}
|
||||
|
||||
// @todo(judah): is this needed?
|
||||
runtime.AddCleanup(&base, func(_ struct{}) {
|
||||
if err := mem.Unreserve(base); err != nil {
|
||||
panic(fmt.Sprintf("paging: failed to release memory - %s", err))
|
||||
}
|
||||
}, struct{}{})
|
||||
|
||||
return func(a Action, size, align uintptr, watermark *uintptr) (unsafe.Pointer, error) {
|
||||
switch a {
|
||||
case ACTION_ALLOC:
|
||||
aligned := mem.AlignForward(size, align)
|
||||
if offset+aligned > total_reserved_in_bytes {
|
||||
return nil, errors.New("paging: out of addressable memory")
|
||||
}
|
||||
|
||||
if offset+aligned > committed {
|
||||
required := offset + aligned
|
||||
to_commit := mem.AlignForward(required, page_size)
|
||||
|
||||
if err := mem.Commit(base[committed:to_commit-committed], mem.PERM_READ|mem.PERM_WRITE); err != nil {
|
||||
return nil, fmt.Errorf("paging: failed to commit memory - %w", err)
|
||||
}
|
||||
|
||||
committed = to_commit
|
||||
}
|
||||
|
||||
ptr := &base[offset]
|
||||
offset += aligned
|
||||
return unsafe.Pointer(ptr), nil
|
||||
|
||||
case ACTION_RESET:
|
||||
if committed > 0 {
|
||||
if err := mem.Decommit(base[:mem.AlignForward(committed, page_size)]); err != nil {
|
||||
return nil, fmt.Errorf("paging: failed to decommit memory - %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
offset = 0
|
||||
committed = 0
|
||||
return nil, nil
|
||||
|
||||
// @todo(judah): should save/restore also decommit memory?
|
||||
|
||||
case ACTION_SAVE:
|
||||
if watermark == nil {
|
||||
return nil, errors.New("paging: cannot save to nil watermark")
|
||||
}
|
||||
|
||||
*watermark = offset
|
||||
return nil, nil
|
||||
|
||||
case ACTION_RESTORE:
|
||||
if watermark == nil {
|
||||
return nil, errors.New("paging: cannot restore nil watermark")
|
||||
}
|
||||
|
||||
clear(base[*watermark:offset])
|
||||
offset = *watermark
|
||||
|
||||
return nil, nil
|
||||
|
||||
default:
|
||||
panic("paging: unimplemented action - " + a.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Nil is an Arena that always returns an error.
|
||||
//
|
||||
// Note: This is useful for tracking usage locations
|
||||
|
|
|
|||
11
mem/mem.go
11
mem/mem.go
|
|
@ -5,7 +5,7 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
Kilobyte = 1 << (10 * (iota + 1))
|
||||
Kilobyte uintptr = 1 << (10 * (iota + 1))
|
||||
Megabyte
|
||||
Gigabyte
|
||||
Terabyte
|
||||
|
|
@ -110,12 +110,3 @@ func Aligned(address uintptr, alignment uintptr) bool {
|
|||
}
|
||||
return address&(alignment-1) == 0
|
||||
}
|
||||
|
||||
// ExtendSlice returns a copy of the given slice, increasing its length, but leaving the capacity intact.
|
||||
func ExtendSlice[T any](slice []T, amount uintptr) []T {
|
||||
if amount+uintptr(len(slice)) > uintptr(cap(slice)) {
|
||||
panic("extendslice: cannot extend slice past its capacity")
|
||||
}
|
||||
|
||||
return slice[: uintptr(len(slice))+amount : cap(slice)]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
//go:build unix
|
||||
|
||||
package mem
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
PERM_NONE = syscall.PROT_NONE
|
||||
PERM_READ = syscall.PROT_READ
|
||||
PERM_WRITE = syscall.PROT_WRITE
|
||||
PERM_EXECUTE = syscall.PROT_EXEC
|
||||
)
|
||||
|
||||
func Reserve(total_address_space uintptr) ([]byte, error) {
|
||||
data, err := syscall.Mmap(-1, 0, int(total_address_space), syscall.PROT_NONE, syscall.MAP_PRIVATE|syscall.MAP_ANON)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func Unreserve(reserved []byte) error {
|
||||
return syscall.Munmap(reserved)
|
||||
}
|
||||
|
||||
func Commit(reserved []byte, perms int) error {
|
||||
return syscall.Mprotect(reserved, perms)
|
||||
}
|
||||
|
||||
func Decommit(committed []byte) (err error) {
|
||||
err = syscall.Mprotect(committed, syscall.PROT_NONE)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return madvise(committed, syscall.MADV_DONTNEED)
|
||||
}
|
||||
|
||||
var _zero uintptr
|
||||
|
||||
func madvise(b []byte, advice int) (err error) {
|
||||
var _p0 unsafe.Pointer
|
||||
if len(b) > 0 {
|
||||
_p0 = unsafe.Pointer(&b[0])
|
||||
} else {
|
||||
_p0 = unsafe.Pointer(&_zero)
|
||||
}
|
||||
|
||||
_, _, e := syscall.Syscall(syscall.SYS_MADVISE, uintptr(_p0), uintptr(len(b)), uintptr(advice))
|
||||
if e != 0 {
|
||||
err = syscall.Errno(e)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
150
union/union.go
Normal file
150
union/union.go
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
package union
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"git.brut.systems/judah/xx/mem"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUninitializedAccess = errors.New("access of uninitialized union")
|
||||
ErrInvalidType = errors.New("type does not exist within union")
|
||||
)
|
||||
|
||||
// anystruct represents a struct type with any members.
|
||||
//
|
||||
// Note: because Go's type constraint system can't enforce
|
||||
// this, anystruct is here for documentation purposes.
|
||||
type anystruct any
|
||||
|
||||
// @note(judah): is there a way to declare the type parameters
|
||||
// to allow 'type Value union.Of[...]' so users can define their
|
||||
// own methods?
|
||||
|
||||
// Of represents a union of different types.
|
||||
//
|
||||
// Since members are accessed by type instead of name,
|
||||
// T is expected to be a struct of types like so:
|
||||
//
|
||||
// type Value = union.Of[struct {
|
||||
// int32
|
||||
// uint32
|
||||
// float32
|
||||
// })
|
||||
type Of[T anystruct] struct {
|
||||
typ reflect.Type
|
||||
mem []byte
|
||||
}
|
||||
|
||||
func (u Of[T]) Size() uintptr {
|
||||
return mem.Sizeof[T]()
|
||||
}
|
||||
|
||||
// String returns the string representation of a union.
|
||||
func (u Of[T]) String() string {
|
||||
var b strings.Builder
|
||||
|
||||
fmt.Fprintf(&b, "union[%s] = ", reflect.TypeFor[T]().String())
|
||||
if u.typ == nil {
|
||||
b.WriteString("none")
|
||||
} else {
|
||||
b.WriteString(u.typ.String())
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// Is returns true if the given type is currently stored in the union.
|
||||
func Is[E any, T anystruct](u Of[T]) bool {
|
||||
// Explicit invalid check to make sure invalid types don't result in false-positives.
|
||||
if u.typ == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return u.typ == reflect.TypeFor[E]()
|
||||
}
|
||||
|
||||
// Set overwrites the backing memory of a union with the given value; initializing the union if uninitialized.
|
||||
//
|
||||
// Set is unsafe and will not verify if the backing memory has enough capacity to store the value.
|
||||
// Use [SetSafe] for more safety checks.
|
||||
func Set[V any, T anystruct](u *Of[T], value V) {
|
||||
if u.mem == nil {
|
||||
u.mem = make([]byte, mem.Sizeof[T]())
|
||||
}
|
||||
|
||||
unsafe.Slice((*V)(unsafe.Pointer(&u.mem[0])), 1)[0] = value
|
||||
u.typ = reflect.TypeFor[V]()
|
||||
}
|
||||
|
||||
// SetSafe overwrites the backing memory of a union with the given value,
|
||||
// returning an error if the value cannot be stored in the union.
|
||||
//
|
||||
// Use [Set] for fewer safety checks.
|
||||
func SetSafe[V any, T anystruct](u *Of[T], value V) error {
|
||||
if u.mem == nil {
|
||||
u.mem = make([]byte, mem.Sizeof[T]())
|
||||
}
|
||||
|
||||
vt := reflect.TypeFor[V]()
|
||||
for _, field := range getInternalFields(*u) {
|
||||
if field.Type == vt {
|
||||
unsafe.Slice((*V)(unsafe.Pointer(&u.mem[0])), 1)[0] = value
|
||||
u.typ = reflect.TypeFor[V]()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("%s - %w", vt, ErrInvalidType)
|
||||
}
|
||||
|
||||
// Get returns the union's backing memory interpreted as a value of type V, panicking if the union is uninitialized.
|
||||
//
|
||||
// Get is unsafe and will not verify if the type exists within the union.
|
||||
// Use [GetSafe] for more safety checks.
|
||||
func Get[V any, T anystruct](u Of[T]) V {
|
||||
if u.mem == nil {
|
||||
panic(ErrUninitializedAccess)
|
||||
}
|
||||
|
||||
return unsafe.Slice((*V)(unsafe.Pointer(&u.mem[0])), 1)[0]
|
||||
}
|
||||
|
||||
// GetSafe returns the union's backing memory interpreted as a value of type V, returning an error if the type
|
||||
// does not exist within the union or the union is uninitialized.
|
||||
//
|
||||
// Use [Get] for fewer safety checks.
|
||||
func GetSafe[V any, T anystruct](u Of[T]) (V, error) {
|
||||
if u.mem == nil {
|
||||
return mem.ZeroValue[V](), ErrUninitializedAccess
|
||||
}
|
||||
|
||||
vt := reflect.TypeFor[V]()
|
||||
for _, field := range getInternalFields(u) {
|
||||
if field.Type == vt {
|
||||
return unsafe.Slice((*V)(unsafe.Pointer(&u.mem[0])), 1)[0], nil
|
||||
}
|
||||
}
|
||||
|
||||
return mem.ZeroValue[V](), ErrInvalidType
|
||||
}
|
||||
|
||||
// getInternalFields returns an array of reflect.StructField belonging
|
||||
// to the internal type of a union.
|
||||
func getInternalFields[U Of[T], T anystruct](_ U) []reflect.StructField {
|
||||
backing := reflect.TypeFor[T]()
|
||||
if backing.Kind() != reflect.Struct {
|
||||
return nil
|
||||
}
|
||||
|
||||
var fields []reflect.StructField
|
||||
for i := range backing.NumField() {
|
||||
fields = append(fields, backing.Field(i))
|
||||
}
|
||||
|
||||
return fields
|
||||
}
|
||||
192
union/union_test.go
Normal file
192
union/union_test.go
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
package union_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.brut.systems/judah/xx/union"
|
||||
)
|
||||
|
||||
func TestUnion_BasicGetSet(t *testing.T) {
|
||||
type Numbers = union.Of[struct {
|
||||
uint8
|
||||
bool
|
||||
}]
|
||||
|
||||
var num Numbers
|
||||
union.Set[uint8](&num, 1)
|
||||
|
||||
b := union.Get[bool](num)
|
||||
if !b {
|
||||
t.Errorf("expected bool value to be true, was %v", b)
|
||||
}
|
||||
|
||||
union.Set(&num, false)
|
||||
|
||||
i := union.Get[uint8](num)
|
||||
if i != 0 {
|
||||
t.Errorf("expected uint8 value to be 0, was %v", i)
|
||||
}
|
||||
}
|
||||
|
||||
type (
|
||||
expr = union.Of[struct {
|
||||
binaryExpr
|
||||
intExpr
|
||||
floatExpr
|
||||
}]
|
||||
binaryExpr struct {
|
||||
Op string
|
||||
Lhs expr
|
||||
Rhs expr
|
||||
}
|
||||
intExpr int64
|
||||
floatExpr float64
|
||||
)
|
||||
|
||||
func TestUnion_OfStructs(t *testing.T) {
|
||||
makeInt := func(value int64) (e expr) {
|
||||
union.Set(&e, intExpr(value))
|
||||
return
|
||||
}
|
||||
makeFloat := func(value float64) (e expr) {
|
||||
union.Set(&e, floatExpr(value))
|
||||
return
|
||||
}
|
||||
makeBinop := func(op string, lhs, rhs expr) (e expr) {
|
||||
union.Set(&e, binaryExpr{
|
||||
Op: op,
|
||||
Lhs: lhs,
|
||||
Rhs: rhs,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
expr1 := makeBinop("+", makeInt(10), makeInt(20))
|
||||
bin1 := union.Get[binaryExpr](expr1)
|
||||
if bin1.Op != "+" {
|
||||
t.Errorf("incorrect op returned from union: %s", bin1.Op)
|
||||
}
|
||||
if lhs := union.Get[intExpr](bin1.Lhs); lhs != 10 {
|
||||
t.Errorf("incorrect lhs returned from union: %v", lhs)
|
||||
}
|
||||
if rhs := union.Get[intExpr](bin1.Rhs); rhs != 20 {
|
||||
t.Errorf("incorrect rhs returned from union: %v", rhs)
|
||||
}
|
||||
|
||||
expr2 := makeBinop("-", expr1, makeFloat(3.14))
|
||||
bin2 := union.Get[binaryExpr](expr2)
|
||||
if bin2.Op != "-" {
|
||||
t.Errorf("incorrect op returned from union of union: %s", bin2.Op)
|
||||
}
|
||||
if lhs := union.Get[binaryExpr](bin2.Lhs); lhs.Op != "+" {
|
||||
t.Errorf("incorrect lhs returned from union of union: %v", lhs)
|
||||
}
|
||||
if rhs := union.Get[floatExpr](bin2.Rhs); rhs != 3.14 {
|
||||
t.Errorf("incorrect rhs returned from union of union: %v", rhs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnion_OfPointers(t *testing.T) {
|
||||
type Value = union.Of[struct {
|
||||
*float64
|
||||
*uint64
|
||||
}]
|
||||
|
||||
var (
|
||||
original uint64 = 100
|
||||
value Value
|
||||
)
|
||||
|
||||
if union.Is[*uint64](value) || union.Is[*float64](value) {
|
||||
t.Error("union internal type was incorrect before usage")
|
||||
}
|
||||
|
||||
union.Set(&value, &original)
|
||||
|
||||
if !union.Is[*uint64](value) {
|
||||
t.Error("union internal type was incorrect after Set")
|
||||
}
|
||||
|
||||
fptr := union.Get[*float64](value)
|
||||
*fptr = 3.14
|
||||
|
||||
if original == 100 {
|
||||
t.Error("original value did not change")
|
||||
}
|
||||
|
||||
uptr := union.Get[*uint64](value)
|
||||
*uptr = 200
|
||||
|
||||
if *fptr == 3.14 {
|
||||
t.Error("float pointer value did not change after modification")
|
||||
}
|
||||
|
||||
if original != 200 {
|
||||
t.Errorf("original value was incorrect: %v", original)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnion_ToString(t *testing.T) {
|
||||
type (
|
||||
Struct = union.Of[struct {
|
||||
int32
|
||||
uint32
|
||||
}]
|
||||
Interface = union.Of[interface {
|
||||
Int()
|
||||
Bool()
|
||||
}]
|
||||
Bool = union.Of[bool]
|
||||
)
|
||||
|
||||
var (
|
||||
s Struct
|
||||
i Interface
|
||||
b Bool
|
||||
)
|
||||
|
||||
if s.String() != "union[none] { int32; uint32 }" {
|
||||
t.Errorf("valid union had invalid stringification: %s", s.String())
|
||||
}
|
||||
|
||||
if i.String() != b.String() {
|
||||
t.Errorf("invalid union had invalid stringification: %s, %s", i.String(), b.String())
|
||||
}
|
||||
|
||||
union.Set[int32](&s, 10)
|
||||
|
||||
if s.String() != "union[int32] { int32; uint32 }" {
|
||||
t.Errorf("valid union had invalid stringification after Set: %s", s.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnion_SafeUsage(t *testing.T) {
|
||||
type Value = union.Of[struct {
|
||||
int32
|
||||
uint32
|
||||
float32
|
||||
}]
|
||||
|
||||
var v Value
|
||||
if _, err := union.GetSafe[int32](v); err == nil {
|
||||
t.Errorf("GetSafe did not error for an uninitialized union")
|
||||
}
|
||||
|
||||
if err := union.SetSafe(&v, false); err == nil {
|
||||
t.Error("SetSafe allowed invalid type")
|
||||
}
|
||||
|
||||
if err := union.SetSafe[int32](&v, 10); err != nil {
|
||||
t.Errorf("SetSafe failed with valid type: %s", err)
|
||||
}
|
||||
|
||||
if _, err := union.GetSafe[bool](v); err == nil {
|
||||
t.Errorf("GetSafe allowed invalid type")
|
||||
}
|
||||
|
||||
if v, err := union.GetSafe[int32](v); err != nil {
|
||||
t.Errorf("GetSafe failed with valid type: %s", err)
|
||||
} else if v != 10 {
|
||||
t.Errorf("GetSafe returned invalid value: %v", v)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue