1
0
Fork 0
forked from judah/xx

mem: add windows memory allocation primitives, add basic tests

This commit is contained in:
Judah Caruso 2026-01-31 12:53:29 -07:00
parent f83a1f323f
commit c6bd701216
4 changed files with 194 additions and 12 deletions

View file

@ -119,3 +119,36 @@ func ExtendSlice[T any](slice []T, amount uintptr) []T {
return slice[: uintptr(len(slice))+amount : cap(slice)]
}
// Access describes memory access permissions.
type Access int
const (
AccessNone Access = 1 << iota
AccessRead
AccessWrite
AccessExecute
)
// Reserve returns a slice of bytes pointing to uncommitted virtual memory.
// The length and capacity of the slice will be total_address_space bytes.
//
// The underlying memory of the slice must be comitted to phyiscal memory before being accessed (see: Commit).
//
// Use Release to return the virtual address space back to the operating system.
func Reserve(total_address_space uintptr) ([]byte, error) { return reserve(total_address_space) }
// Release returns reserved virtual address space back to the operating system.
//
// Note: Any committed memory within its address space will be freed as well.
func Release(reserved []byte) error { return release(reserved) }
// Commit maps virtual memory to physical memory.
func Commit(reserved []byte, access Access) error { return commit(reserved, access) }
// Decommit unmaps committed memory, leaving the underlying addresss space intact.
//
// Decommitted memory can be re-committed at a later time using Commit.
//
// Note: Accessing the memory after calling Decommit is unsafe and may cause a panic.
func Decommit(committed []byte) (err error) { return decommit(committed) }

View file

@ -2,8 +2,10 @@ package mem_test
import (
"testing"
"unsafe"
"git.brut.systems/judah/xx/mem"
"git.brut.systems/judah/xx/testx"
)
func TestBitCast(t *testing.T) {
@ -24,3 +26,53 @@ func TestBitCast(t *testing.T) {
t.Fail()
}
}
func TestAllocationPrimitives(t *testing.T) {
t.Run("reserve, unreserve", func(t *testing.T) {
data, err := mem.Reserve(1 * mem.Gigabyte)
testx.Expect(t, err == nil, "mem.Reserve returned an error - %s", err)
testx.Expect(t, len(data) == 1*mem.Gigabyte, "len was %d", len(data))
testx.Expect(t, cap(data) == 1*mem.Gigabyte, "len was %d", cap(data))
err = mem.Release(data)
testx.Expect(t, err == nil, "mem.Unreserve returned an error - %s", err)
})
t.Run("commit", func(t *testing.T) {
data, err := mem.Reserve(1 * mem.Gigabyte)
testx.Expect(t, err == nil, "mem.Reserve returned an error - %s", err)
err = mem.Commit(data, mem.AccessRead|mem.AccessWrite)
testx.Expect(t, err == nil, "mem.Commit returned an error - %s", err)
for i := range data {
data[i] = byte(i * i)
}
})
t.Run("decommit", func(t *testing.T) {
data, err := mem.Reserve(1 * mem.Gigabyte)
testx.Expect(t, err == nil, "mem.Reserve returned an error - %s", err)
err = mem.Commit(data, mem.AccessRead|mem.AccessWrite)
testx.Expect(t, err == nil, "mem.Commit returned an error - %s", err)
before := uintptr(unsafe.Pointer(&data[0]))
err = mem.Decommit(data)
testx.Expect(t, err == nil, "mem.Decommit returned an error - %s", err)
// accessing data before recommitting it will fail
err = mem.Commit(data, mem.AccessRead|mem.AccessWrite)
testx.Expect(t, err == nil, "mem.Commit returned an error - %s", err)
after := uintptr(unsafe.Pointer(&data[0]))
testx.Expect(t, before == after, "base pointers did not match between after recommit %d != %d", before, after)
for i := range data {
data[i] = byte(i * i)
}
})
}

View file

@ -7,14 +7,7 @@ import (
"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) {
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
@ -23,15 +16,15 @@ func Reserve(total_address_space uintptr) ([]byte, error) {
return data, nil
}
func Unreserve(reserved []byte) error {
func release(reserved []byte) error {
return syscall.Munmap(reserved)
}
func Commit(reserved []byte, perms int) error {
return syscall.Mprotect(reserved, perms)
func commit(reserved []byte, access Access) error {
return syscall.Mprotect(reserved, access_to_prot(access))
}
func Decommit(committed []byte) (err error) {
func decommit(committed []byte) (err error) {
err = syscall.Mprotect(committed, syscall.PROT_NONE)
if err != nil {
return
@ -57,3 +50,19 @@ func madvise(b []byte, advice int) (err error) {
return
}
func access_to_prot(access Access) (prot int) {
prot = syscall.PROT_NONE
if access&AccessRead != 0 {
prot |= syscall.PROT_READ
}
if access&AccessWrite != 0 {
prot |= syscall.PROT_WRITE
}
if access&AccessExecute != 0 {
prot |= syscall.PROT_EXEC
}
return
}

88
mem/mem_windows.go Normal file
View file

@ -0,0 +1,88 @@
//go:build windows
package mem
import (
"syscall"
"unsafe"
)
func reserve(total_address_space uintptr) ([]byte, error) {
addr, _, err := _VirtualAlloc.Call(0, total_address_space, _MEM_RESERVE, _PAGE_NOACCESS)
if addr == 0 {
return nil, err
}
return unsafe.Slice((*byte)(unsafe.Pointer(addr)), total_address_space), nil
}
func release(reserved []byte) error {
res, _, err := _VirtualFree.Call(uintptr(unsafe.Pointer(&reserved[0])), 0, _MEM_RELEASE)
if res == 0 {
return err
}
return nil
}
func commit(reserved []byte, access Access) error {
ret, _, err := _VirtualAlloc.Call(
uintptr(unsafe.Pointer(&reserved[0])),
uintptr(len(reserved)),
_MEM_COMMIT,
uintptr(access_to_prot(access)),
)
if ret == 0 {
return err
}
return nil
}
func decommit(committed []byte) error {
ret, _, err := _VirtualFree.Call(
uintptr(unsafe.Pointer(&committed[0])),
uintptr(len(committed)),
_MEM_DECOMMIT,
)
if ret == 0 {
return err
}
return nil
}
var (
kernel32 = syscall.NewLazyDLL("kernel32.dll")
_VirtualAlloc = kernel32.NewProc("VirtualAlloc")
_VirtualFree = kernel32.NewProc("VirtualFree")
)
const (
_MEM_COMMIT = 0x1000
_MEM_RESERVE = 0x2000
_MEM_DECOMMIT = 0x4000
_MEM_RELEASE = 0x8000
_PAGE_NOACCESS = 0x01
_PAGE_READONLY = 0x02
_PAGE_READWRITE = 0x04
_PAGE_EXECUTE_READ = 0x20
_PAGE_EXECUTE_READWRITE = 0x40
)
func access_to_prot(access Access) uint32 {
switch access {
case AccessRead | AccessWrite | AccessExecute:
return _PAGE_EXECUTE_READWRITE
case AccessRead | AccessExecute:
return _PAGE_EXECUTE_READ
case AccessRead | AccessWrite:
return _PAGE_READWRITE
case AccessRead:
return _PAGE_READONLY
default:
return _PAGE_NOACCESS
}
}