Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cabi: add Canonical ABI realloc (cabi_realloc) #3

Merged
merged 20 commits into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/go.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ jobs:
- name: Set up TinyGo
uses: acifani/[email protected]
with:
tinygo-version: '0.30.0'
tinygo-version: "0.30.0"

- name: Set up Wasmtime
uses: bytecodealliance/actions/wasmtime/[email protected]
with:
version: "v13.0.0"

- name: Vet Go code
run: go vet ./...
Expand Down
21 changes: 21 additions & 0 deletions cabi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
### Usage

The `cabi` package contains a single exported WebAssembly function `cabi_realloc` ([Canonical ABI] realloc). To use, import this package with `_`:

```
import _ "github.com/ydnar/wasm-tools-go/cabi"
```

`cabi_realloc` is a WebAssembly [core function](https://www.w3.org/TR/wasm-core-2/syntax/modules.html#functions) that is validated to have the following core function type:

```
(func (param $originalPtr i32)
(param $originalSize i32)
(param $alignment i32)
(param $newSize i32)
(result i32))
```

The [Canonical ABI] will use realloc both to allocate (passing 0 for the first two parameters) and reallocate. If the Canonical ABI needs realloc, validation requires this option to be present (there is no default).

[Canonical ABI]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/CanonicalABI.md
52 changes: 52 additions & 0 deletions cabi/realloc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package cabi

import "unsafe"

// realloc allocates or reallocates memory for Component Model calls across
// the host-guest boundary.
//
// Note: the use of uintptr assumes 32-bit pointers, e.g. GOOS=wasm32 when compiled for WebAssembly.
//
//go:export cabi_realloc
//go:wasmexport cabi_realloc
func realloc(ptr unsafe.Pointer, size, align, newsize uintptr) unsafe.Pointer {
if newsize <= size {
return unsafe.Add(ptr, offset(uintptr(ptr), align))
}
newptr := alloc(newsize, align)
if size > 0 {
copy(unsafe.Slice((*byte)(newptr), newsize), unsafe.Slice((*byte)(ptr), size))
}
return newptr
}

// offset returns the delta between the aligned value of ptr and ptr
// so it can be passed to unsafe.Add. The return value is guaranteed to be >= 0.
func offset(ptr, align uintptr) uintptr {
newptr := (ptr + align - 1) &^ (align - 1)
return newptr - ptr
}

// alloc allocates a block of memory with size bytes.
// It attempts to align the allocated memory by allocating a slice of
// a type that matches the desired alignment. It aligns to 16 bytes for
// values of align other than 1, 2, 4, or 8.
func alloc(size, align uintptr) unsafe.Pointer {
switch align {
case 1:
s := make([]uint8, size)
return unsafe.Pointer(unsafe.SliceData(s))
case 2:
s := make([]uint16, min(size/align, 1))
return unsafe.Pointer(unsafe.SliceData(s))
case 4:
s := make([]uint32, min(size/align, 1))
return unsafe.Pointer(unsafe.SliceData(s))
case 8:
s := make([]uint64, min(size/align, 1))
return unsafe.Pointer(unsafe.SliceData(s))
default:
s := make([][16]uint8, min(size/align, 1))
return unsafe.Pointer(unsafe.SliceData(s))
}
}
72 changes: 72 additions & 0 deletions cabi/realloc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package cabi

import (
"slices"
"testing"
"unsafe"
)

func TestRealloc(t *testing.T) {
const threshold = 16

tests := []struct {
name string
ptr uintptr
size uintptr
align uintptr
newsize uintptr
want uintptr
}{
{"nil", 0, 0, 1, 0, 0},
{"nil with align", 0, 0, 2, 0, 0},
{"align to 2", 1, 0, 2, 0, 2},
{"align to 2", 1, 0, 2, 0, 2},
{"align to 4", 1, 0, 4, 0, 4},
{"align to 8", 3, 0, 8, 0, 8},
{"align to 8", 9, 0, 16, 0, 16},
{"alloc 100 bytes", 0, 0, 1, 100, sliceData(hundred)},
{"preserve 5 bytes", stringData("hello"), 5, 1, 5, stringData("hello")},
{"shrink 8 bytes to 4", stringData("fourfour"), 8, 1, 4, stringData("four")},
{"expand 4 bytes to 8", stringData("zero"), 4, 1, 8, stringData("zero\u0000\u0000\u0000\u0000")},
{"cut down lorem ipsum", stringData(lorem), 4, 1, 200, stringData(lorem[:200])},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := realloc(unsafePointer(tt.ptr), tt.size, tt.align, tt.newsize)
if (tt.want < threshold && uintptr(got) != tt.want) || (tt.want >= threshold && uintptr(got) < threshold) {
t.Errorf("realloc(%d, %d, %d, %d): expected %d, got %d",
tt.ptr, tt.size, tt.align, tt.newsize, tt.want, got)
}
if uintptr(got)%tt.align != 0 {
t.Errorf("expected ptr aligned to %d, got %d", tt.align, uintptr(got)&tt.align)
}
if uintptr(got) < threshold {
return // it didn’t allocate
}
if tt.ptr == 0 {
wants := unsafe.Slice((*byte)(unsafePointer(tt.want)), tt.newsize)
gots := unsafe.Slice((*byte)(got), tt.newsize)
if slices.Compare(wants, gots) != 0 {
t.Errorf("expected %v, got %v", wants, gots)
}
}
})
}
}

var hundred = make([]byte, 100)
var lorem = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut volutpat arcu eu est tristique suscipit. Nulla laoreet purus magna, at feugiat tortor fermentum non. Integer semper et magna id placerat. Quisque purus lorem, mollis vel convallis eu, ullamcorper sit amet justo. Duis tempus gravida lacus, vel dapibus augue. Nunc sed condimentum lacus. Cras vulputate cursus dictum. Etiam felis metus, volutpat id luctus ac, ultrices nec metus. Proin sagittis nulla a pretium sagittis. Nullam tristique sapien sed semper faucibus. Fusce condimentum nulla dui. Donec egestas nunc in blandit mollis.`

// Appease vet, see https://github.com/golang/go/issues/58625
func unsafePointer(p uintptr) unsafe.Pointer {
return *(*unsafe.Pointer)(unsafe.Pointer(&p))
}

func sliceData[T any](s []T) uintptr {
return uintptr(unsafe.Pointer(unsafe.SliceData(s)))
}

func stringData(s string) uintptr {
return uintptr(unsafe.Pointer(unsafe.StringData(s)))
}
4 changes: 1 addition & 3 deletions wit/abi.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ package wit

// Align aligns ptr with alignment align.
func Align(ptr, align uintptr) uintptr {
// (dividend + divisor - 1) / divisor
// http://www.cs.nott.ac.uk/~rcb/G51MPC/slides/NumberLogic.pdf
return ((ptr + align - 1) / align) * align
return (ptr + align - 1) &^ (align - 1)
}

// Discriminant returns the smallest WIT integer type that can represent 0...n.
Expand Down