Skip to content

Commit

Permalink
Updates TinyGo to use Go 1.20 features (#26)
Browse files Browse the repository at this point in the history
TinyGo 0.27 doesn't fully support Go 1.20, but it supports what we need.
Particularly, `unsafe.SliceData` and `unsafe.StringData` were added to
TinyGo 0.27 ahead of the more complete Go 1.20 version: 0.28 (not yet
out).

Using the approach elaborated in wazero [1.2.0](https://github.com/tetratelabs/wazero/releases/tag/v1.2.0)
TinyGo examples resulted in significant performance benefits, benefits
far more than just the part around the runtime.

Signed-off-by: Adrian Cole <[email protected]>
  • Loading branch information
codefromthecrypt authored Jun 5, 2023
1 parent d3dc43a commit ae25bc5
Show file tree
Hide file tree
Showing 32 changed files with 111 additions and 88 deletions.
7 changes: 3 additions & 4 deletions .github/workflows/commit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
strategy:
matrix:
go-version: # Note: Go only supports 2 versions: https://go.dev/doc/devel/release#policy
- "1.19"
- "1.20"

steps:
- uses: actions/checkout@v3
Expand All @@ -50,17 +50,16 @@ jobs:
strategy:
matrix:
go-version: # Note: Go only supports 2 versions: https://go.dev/doc/devel/release#policy
- "1.19"
- "1.20"
tinygo-version: # Note: TinyGo only supports latest: https://github.com/tinygo-org/tinygo/releases
- "0.27.0" # Latest

steps:
- uses: actions/checkout@v3

- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go-version }}
cache: true

- name: "Set up TinyGo"
run: | # Installing via curl so commands are similar on OS/x
Expand Down
9 changes: 4 additions & 5 deletions .github/workflows/testdata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,16 @@ jobs:
strategy:
matrix:
go-version: # Note: Go only supports 2 versions: https://go.dev/doc/devel/release#policy
- "1.19"
- "1.20"
tinygo-version: # Note: TinyGo only supports latest: https://github.com/tinygo-org/tinygo/releases
- "0.27.0" # Latest

steps:
- uses: actions/checkout@v3

- uses: actions/setup-go@v3
with: # Not cache: true because we also cache go-build and golangci-lint
- uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go-version }}
cache: true

- name: "Set up TinyGo"
run: | # Installing via curl so commands are similar on OS/x
Expand All @@ -48,7 +47,7 @@ jobs:
- name: "Set up wat2wasm"
run: | # Needed for testdata. wabt includes wat2wasm.
wabt_version=1.0.32
wabt_version=1.0.33
wabt_url=https://github.com/WebAssembly/wabt/releases/download/${wabt_version}/wabt-${wabt_version}-ubuntu.tar.gz
curl -sSL ${wabt_url} | tar --strip-components 2 -C /usr/local/bin -xzf - wabt-${wabt_version}/bin/wat2wasm
Expand Down
2 changes: 1 addition & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This is built using - https://golangci-lint.run/usage/configuration/
run:
go: '1.19'
go: '1.20'
modules-download-mode: readonly
tests: false

13 changes: 5 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
gosimports := github.com/rinchsan/gosimports/cmd/[email protected]
golangci_lint := github.com/golangci/golangci-lint/cmd/[email protected]
gofumpt := mvdan.cc/[email protected]
gosimports := github.com/rinchsan/gosimports/cmd/[email protected]
golangci_lint := github.com/golangci/golangci-lint/cmd/[email protected]

.PHONY: testdata
testdata:
Expand Down Expand Up @@ -43,12 +44,8 @@ lint: $(golangci_lint_path)

.PHONY: format
format:
@find . -type f -name '*.go' | xargs gofmt -s -w
@for f in `find . -name '*.go'`; do \
awk '/^import \($$/,/^\)$$/{if($$0=="")next}{print}' $$f > /tmp/fmt; \
mv /tmp/fmt $$f; \
done
@go run $(gosimports) -local github.com/http-wasm/http-wasm-guest-tinygo -w $(shell find . -name '*.go' -type f)
@go run $(gofumpt) -l -w .
@go run $(gosimports) -local github.com/http-wasm/ -w $(shell find . -name '*.go' -type f)

.PHONY: check
check:
Expand Down
Binary file modified examples/router/main.wasm
Binary file not shown.
Binary file modified examples/wasi/main.wasm
Binary file not shown.
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
module github.com/http-wasm/http-wasm-guest-tinygo

go 1.19
// TinyGo 0.27 doesn't fully support Go 1.20, but it supports what we need.
// Particularly, unsafe.SliceData, unsafe.StringData were added to TinyGo 0.27.
// See https://github.com/tinygo-org/tinygo/commit/c43958972c3ffcd51e65414a346e53779edb9f97
go 1.20
26 changes: 14 additions & 12 deletions handler/body.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package handler

import (
"io"
"unsafe"
"runtime"

"github.com/http-wasm/http-wasm-guest-tinygo/handler/api"
"github.com/http-wasm/http-wasm-guest-tinygo/handler/internal/imports"
Expand Down Expand Up @@ -40,13 +40,14 @@ func (b wasmBody) WriteTo(w io.Writer) (written uint64, err error) {

// ReadN implements the same method as documented on api.Body.
func (b wasmBody) Read(bytes []byte) (size uint32, eof bool) {
limit := uint32(len(bytes))
if limit == 0 { // invalid, but prevent crashing.
return 0, false
ptr, limit := mem.SliceToPtr(bytes)
if limit == 0 {
return // invalid, but prevent crashing.
}

ptr := uintptr(unsafe.Pointer(&bytes[0])) // TODO: mem.SliceToPtr
return read(b, ptr, limit)
size, eof = read(b, uintptr(ptr), limit)
runtime.KeepAlive(bytes) // keep bytes alive until ptr is no longer needed.
return
}

func read(b wasmBody, ptr uintptr, limit imports.BufLimit) (size uint32, eof bool) {
Expand All @@ -58,21 +59,22 @@ func read(b wasmBody, ptr uintptr, limit imports.BufLimit) (size uint32, eof boo

// Write implements the same method as documented on api.Body.
func (b wasmBody) Write(bytes []byte) {
size := uint32(len(bytes))
if size == 0 { // invalid, but prevent crashing.
ptr, size := mem.SliceToPtr(bytes)
if size == 0 {
return
}

ptr := uintptr(unsafe.Pointer(&bytes[0])) // TODO: mem.SliceToPtr
imports.WriteBody(imports.BodyKind(b), ptr, size)
imports.WriteBody(imports.BodyKind(b), uintptr(ptr), size)
runtime.KeepAlive(bytes) // keep bytes alive until ptr is no longer needed.
}

// WriteString implements the same method as documented on api.Body.
func (b wasmBody) WriteString(s string) {
ptr, size := mem.StringToPtr(s)
if size == 0 { // invalid, but prevent crashing.
if size == 0 {
return
}

imports.WriteBody(imports.BodyKind(b), ptr, size)
imports.WriteBody(imports.BodyKind(b), uintptr(ptr), size)
runtime.KeepAlive(s) // keep s alive until ptr is no longer needed.
}
33 changes: 23 additions & 10 deletions handler/header.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package handler

import (
"runtime"
"unsafe"

"github.com/http-wasm/http-wasm-guest-tinygo/handler/api"
Expand Down Expand Up @@ -30,9 +31,11 @@ func (w wasmHeader) Names() (names []string) {
}
// Otherwise, we have to allocate a new buffer for the large entry.
buf := make([]byte, size)
ptr := uintptr(unsafe.Pointer(&buf[0]))
_ = imports.GetHeaderNames(imports.HeaderKind(w), ptr, size)
return mem.GetNULTerminated(buf)
ptr := unsafe.Pointer(unsafe.SliceData(buf))
_ = imports.GetHeaderNames(imports.HeaderKind(w), uintptr(ptr), size)
names = mem.GetNULTerminated(buf)
runtime.KeepAlive(buf) // keep buf alive until ptr is no longer needed.
return
}

// Get implements the same method as documented on api.Request.
Expand All @@ -48,10 +51,12 @@ func (w wasmHeader) Get(name string) (value string, ok bool) {
// GetAll implements the same method as documented on api.Request.
func (w wasmHeader) GetAll(name string) (names []string) {
namePtr, nameSize := mem.StringToPtr(name)
countLen := imports.GetHeaderValues(imports.HeaderKind(w), namePtr, nameSize, mem.ReadBufPtr, mem.ReadBufLimit)
countLen := imports.GetHeaderValues(imports.HeaderKind(w), uintptr(namePtr), nameSize, mem.ReadBufPtr, mem.ReadBufLimit)
runtime.KeepAlive(name) // keep name alive until ptr is no longer needed.
if countLen == 0 {
return
}

size := uint32(countLen)
if size == 0 {
return
Expand All @@ -61,27 +66,35 @@ func (w wasmHeader) GetAll(name string) (names []string) {
}
// Otherwise, we have to allocate a new buffer for the large entry.
buf := make([]byte, size)
ptr := uintptr(unsafe.Pointer(&buf[0]))
_ = imports.GetHeaderValues(imports.HeaderKind(w), namePtr, nameSize, ptr, size)
return mem.GetNULTerminated(buf)
ptr := unsafe.Pointer(unsafe.SliceData(buf))
_ = imports.GetHeaderValues(imports.HeaderKind(w), uintptr(namePtr), nameSize, uintptr(ptr), size)
names = mem.GetNULTerminated(buf)
runtime.KeepAlive(name) // keep name alive until ptr is no longer needed.
runtime.KeepAlive(buf) // keep buf alive until ptr is no longer needed.
return
}

// Set implements the same method as documented on api.Request.
func (w wasmHeader) Set(name, value string) {
namePtr, nameSize := mem.StringToPtr(name)
valuePtr, valueSize := mem.StringToPtr(value)
imports.SetHeaderValue(imports.HeaderKind(w), namePtr, nameSize, valuePtr, valueSize)
imports.SetHeaderValue(imports.HeaderKind(w), uintptr(namePtr), nameSize, uintptr(valuePtr), valueSize)
runtime.KeepAlive(name) // keep name alive until ptr is no longer needed.
runtime.KeepAlive(value) // keep value alive until ptr is no longer needed.
}

// Add implements the same method as documented on api.Request.
func (w wasmHeader) Add(name, value string) {
namePtr, nameSize := mem.StringToPtr(name)
valuePtr, valueSize := mem.StringToPtr(value)
imports.AddHeaderValue(imports.HeaderKind(w), namePtr, nameSize, valuePtr, valueSize)
imports.AddHeaderValue(imports.HeaderKind(w), uintptr(namePtr), nameSize, uintptr(valuePtr), valueSize)
runtime.KeepAlive(name) // keep name alive until ptr is no longer needed.
runtime.KeepAlive(value) // keep value alive until ptr is no longer needed.
}

// Remove implements the same method as documented on api.Request.
func (w wasmHeader) Remove(name string) {
namePtr, nameSize := mem.StringToPtr(name)
imports.RemoveHeader(imports.HeaderKind(w), namePtr, nameSize)
imports.RemoveHeader(imports.HeaderKind(w), uintptr(namePtr), nameSize)
runtime.KeepAlive(name) // keep name alive until ptr is no longer needed.
}
5 changes: 4 additions & 1 deletion handler/host.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package handler

import (
"runtime"

"github.com/http-wasm/http-wasm-guest-tinygo/handler/api"
"github.com/http-wasm/http-wasm-guest-tinygo/handler/internal/imports"
"github.com/http-wasm/http-wasm-guest-tinygo/handler/internal/mem"
Expand Down Expand Up @@ -36,5 +38,6 @@ func (wasmHost) Log(level api.LogLevel, message string) {
return // don't incur host call overhead
}
ptr, size := mem.StringToPtr(message)
imports.Log(level, ptr, size)
imports.Log(level, uintptr(ptr), size)
runtime.KeepAlive(message) // keep message alive until ptr is no longer needed.
}
57 changes: 34 additions & 23 deletions handler/internal/mem/mem.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package mem

import (
"runtime"
"unsafe"

"github.com/http-wasm/http-wasm-guest-tinygo/handler/internal/imports"
Expand All @@ -15,50 +16,60 @@ var (
ReadBufLimit = uint32(2048)
)

// SliceToPtr returns a pointer and size pair for the given slice in a way
// compatible with WebAssembly numeric types.
// The returned pointer aliases the slice hence the slice must be kept alive
// until ptr is no longer needed.
func SliceToPtr(b []byte) (uint32, uint32) {
ptr := unsafe.Pointer(unsafe.SliceData(b))
return uint32(uintptr(ptr)), uint32(len(b))
}

// StringToPtr returns a pointer and size pair for the given string in a way
// compatible with WebAssembly numeric types.
func StringToPtr(s string) (uintptr, uint32) {
if s == "" {
return ReadBufPtr, 0
}
buf := []byte(s)
ptr := &buf[0]
unsafePtr := uintptr(unsafe.Pointer(ptr))
return unsafePtr, uint32(len(buf))
// The returned pointer aliases the string hence the string must be kept alive
// until ptr is no longer needed.
func StringToPtr(s string) (uint32, uint32) {
ptr := unsafe.Pointer(unsafe.StringData(s))
return uint32(uintptr(ptr)), uint32(len(s))
}

// GetString copies a string from the bytes returned by fn, so that it can
// safely be used without risk of corruption.
func GetString(fn func(ptr uintptr, limit imports.BufLimit) (len uint32)) (result string) {
size := fn(ReadBufPtr, ReadBufLimit)
if size == 0 {
return
}
if size > 0 && size <= ReadBufLimit {
return // If nothing was read, return an empty string.
} else if size <= ReadBufLimit {
return string(ReadBuf[:size]) // string will copy the buffer.
}

// Otherwise, allocate a new string
buf := make([]byte, size)
ptr := uintptr(unsafe.Pointer(&buf[0]))
_ = fn(ptr, size)
s := unsafe.Slice((*byte)(unsafe.Pointer(ptr)), size)
return *(*string)(unsafe.Pointer(&s))
ptr := unsafe.Pointer(unsafe.SliceData(buf))
_ = fn(uintptr(ptr), size)
result = *(*string)(ptr) // don't return string(buf) as that copies buf.
runtime.KeepAlive(buf) // keep buf alive until ptr is no longer needed.
return
}

// GetBytes copies the bytes returned by fn, so that they can safely be used
// without risk of corruption.
func GetBytes(fn func(ptr uintptr, limit imports.BufLimit) (len uint32)) (result []byte) {
size := fn(ReadBufPtr, ReadBufLimit)
if size == 0 {
return
}
if size > 0 && size <= ReadBufLimit {
// copy to avoid passing a mutable buffer
return // If nothing was read, return a nil slice.
} else if size <= ReadBufLimit {
// copy to avoid passing out our read buffer
result = make([]byte, size)
copy(result, ReadBuf)
return
}
buf := make([]byte, size)
ptr := uintptr(unsafe.Pointer(&buf[0]))
_ = fn(ptr, size)
return buf

result = make([]byte, size)
ptr := unsafe.Pointer(unsafe.SliceData(result))
_ = fn(uintptr(ptr), size)
return
}

func GetNULTerminated(b []byte) (entries []string) {
Expand Down
8 changes: 6 additions & 2 deletions handler/request.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package handler

import (
"runtime"

"github.com/http-wasm/http-wasm-guest-tinygo/handler/api"
"github.com/http-wasm/http-wasm-guest-tinygo/handler/internal/imports"
"github.com/http-wasm/http-wasm-guest-tinygo/handler/internal/mem"
Expand All @@ -20,7 +22,8 @@ func (wasmRequest) GetMethod() string {
// SetMethod implements the same method as documented on api.Request.
func (wasmRequest) SetMethod(method string) {
ptr, size := mem.StringToPtr(method)
imports.SetMethod(ptr, size)
imports.SetMethod(uintptr(ptr), size)
runtime.KeepAlive(method) // keep method alive until ptr is no longer needed.
}

// GetURI implements the same method as documented on api.Request.
Expand All @@ -31,7 +34,8 @@ func (wasmRequest) GetURI() string {
// SetURI implements the same method as documented on api.Request.
func (wasmRequest) SetURI(uri string) {
ptr, size := mem.StringToPtr(uri)
imports.SetURI(ptr, size)
imports.SetURI(uintptr(ptr), size)
runtime.KeepAlive(uri) // keep uri alive until ptr is no longer needed.
}

// GetProtocolVersion implements the same method as documented on api.Request.
Expand Down
3 changes: 1 addition & 2 deletions internal/e2e/benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ import (
"net/http"
"testing"

nethttp "github.com/http-wasm/http-wasm-host-go/handler/nethttp"

"github.com/http-wasm/http-wasm-guest-tinygo/internal/test"
nethttp "github.com/http-wasm/http-wasm-host-go/handler/nethttp"
)

var (
Expand Down
Loading

0 comments on commit ae25bc5

Please sign in to comment.