Compare commits

...
Author SHA1 Message Date
moloch-- d09c60089f Remove info logs, update unit tests 2023-04-09 08:59:07 -07:00
moloch-- a97d7caf55 implemented wasm sqlite3, remove cgo from server builds 2023-04-09 08:14:04 -07:00
209 changed files with 61688 additions and 23 deletions

No files matched your search

+3 -20
View File
@@ -5,7 +5,7 @@
GO ?= go
ARTIFACT_SUFFIX ?=
ENV =
TAGS ?= -tags osusergo,netgo,cgosqlite,sqlite_omit_load_extension
TAGS ?= -tags osusergo,netgo,wasm_sqlite
#
@@ -63,17 +63,6 @@ STATIC_TARGET := linux
UNAME_S := $(shell uname -s)
UNAME_P := $(shell uname -p)
# If the target is Windows from Linux/Darwin, check for mingw
CROSS_COMPILERS = x86_64-w64-mingw32-gcc x86_64-w64-mingw32-g++
ifneq (,$(findstring cgosqlite,$(TAGS)))
ENV +=CGO_ENABLED=1
ifeq ($(MAKECMDGOALS), windows)
K := $(foreach exec,$(CROSS_COMPILERS),\
$(if $(shell which $(exec)),some string,$(error "Missing cross-compiler $(exec) in PATH")))
ENV += CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++
endif
endif
# Programs required for generating protobuf/grpc files
PB_COMPILERS = protoc protoc-gen-go protoc-gen-go-grpc
ifeq ($(MAKECMDGOALS), pb)
@@ -108,12 +97,6 @@ ifeq ($(MAKECMDGOALS), linux)
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
endif
ifeq ($(MAKECMDGOALS), linux-arm64)
# Redefine TAGS/ENV to use pure go sqlite3
TAGS = -tags osusergo,netgo,gosqlite
ENV = CGO_ENABLED=0
endif
#
# Targets
#
@@ -144,8 +127,8 @@ linux-arm64: clean .downloaded_assets validate-go-version
.PHONY: windows
windows: clean .downloaded_assets validate-go-version
GOOS=windows $(ENV) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX).exe ./server
GOOS=windows $(ENV) $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX).exe ./client
GOOS=windows GOARCH=amd64 $(ENV) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX).exe ./server
GOOS=windows GOARCH=amd64 $(ENV) $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX).exe ./client
.PHONY: pb
pb:
+1 -1
View File
@@ -21,7 +21,7 @@ echo "----------------------------------------------------------------"
echo "WARNING: Running unit tests on slow systems can take a LONG time"
echo " Recommended to only run on 16+ CPU cores and 32Gb+ RAM"
echo "----------------------------------------------------------------"
TAGS=osusergo,netgo,cgosqlite,sqlite_omit_load_extension
TAGS=osusergo,netgo,wasm_sqlite
## Client
+3
View File
@@ -31,6 +31,7 @@ require (
github.com/mattn/go-sqlite3 v1.14.16
github.com/miekg/dns v1.1.52
github.com/moloch--/memmod v0.0.0-20211120144554-8b37cc654945
github.com/ncruces/go-sqlite3 v0.5.3
github.com/pquerna/otp v1.4.0
github.com/sirupsen/logrus v1.9.0
github.com/spf13/cobra v1.6.1
@@ -96,10 +97,12 @@ require (
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/ncruces/julianday v0.1.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tetratelabs/wazero v1.0.0 // indirect
github.com/thedevsaddam/gojsonq/v2 v2.5.2 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect
+6
View File
@@ -222,6 +222,10 @@ github.com/moloch--/grumble v1.1.6/go.mod h1:r7j3ShNy5EmOsegRD2DzTutIaGiLiA3M5yB
github.com/moloch--/memmod v0.0.0-20211120144554-8b37cc654945 h1:m3yCfV8Vqp4MF1B+gPPjbjINdufl0UXqyYplE0aGhx8=
github.com/moloch--/memmod v0.0.0-20211120144554-8b37cc654945/go.mod h1:1grVt4HaTofvhFUZYtofeRbGXfczNwCie9MYoM4lP/o=
github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
github.com/ncruces/go-sqlite3 v0.5.3 h1:n7kYgRp5CVZepOiZql/pgPG3IelkUIF95AE70EiM8JE=
github.com/ncruces/go-sqlite3 v0.5.3/go.mod h1:bC0Wm1XQMcTz7BZzINEYFHLvfzxh+NbZftPo40xkfuc=
github.com/ncruces/julianday v0.1.5 h1:hDJ9ejiMp3DHsoZ5KW4c1lwfMjbARS7u/gbYcd0FBZk=
github.com/ncruces/julianday v0.1.5/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
@@ -264,6 +268,8 @@ github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tetratelabs/wazero v1.0.0 h1:sCE9+mjFex95Ki6hdqwvhyF25x5WslADjDKIFU5BXzI=
github.com/tetratelabs/wazero v1.0.0/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ=
github.com/thedevsaddam/gojsonq/v2 v2.5.2 h1:CoMVaYyKFsVj6TjU6APqAhAvC07hTI6IQen8PHzHYY0=
github.com/thedevsaddam/gojsonq/v2 v2.5.2/go.mod h1:bv6Xa7kWy82uT0LnXPE2SzGqTj33TAEeR560MdJkiXs=
github.com/things-go/go-socks5 v0.0.3 h1:QtlIhkwDuLNCwW3wnt2uTjn1mQzpyjnwct2xdPuqroI=
@@ -1,4 +1,4 @@
//go:build cgosqlite
//go:build cgo_sqlite
package db
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build gosqlite
//go:build go_sqlite
package db
+44
View File
@@ -0,0 +1,44 @@
//go:build wasm_sqlite
package db
/*
Sliver Implant Framework
Copyright (C) 2021 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"github.com/bishopfox/sliver/server/configs"
_ "github.com/ncruces/go-sqlite3"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func sqliteClient(dbConfig *configs.DatabaseConfig) *gorm.DB {
dsn, err := dbConfig.DSN()
if err != nil {
panic(err)
}
dbClient, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{
PrepareStmt: true,
Logger: getGormLogger(dbConfig),
})
if err != nil {
panic(err)
}
return dbClient
}
+16
View File
@@ -0,0 +1,16 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
tools
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Nuno Cruces
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+83
View File
@@ -0,0 +1,83 @@
# Go bindings to SQLite using Wazero
[![Go Reference](https://pkg.go.dev/badge/image)](https://pkg.go.dev/github.com/ncruces/go-sqlite3)
[![Go Report](https://goreportcard.com/badge/github.com/ncruces/go-sqlite3)](https://goreportcard.com/report/github.com/ncruces/go-sqlite3)
[![Go Coverage](https://github.com/ncruces/go-sqlite3/wiki/coverage.svg)](https://github.com/ncruces/go-sqlite3/wiki/Test-coverage-report)
Go module `github.com/ncruces/go-sqlite3` wraps a [WASM](https://webassembly.org/) build of [SQLite](https://sqlite.org/),
and uses [wazero](https://wazero.io/) to provide `cgo`-free SQLite bindings.
- Package [`github.com/ncruces/go-sqlite3`](https://pkg.go.dev/github.com/ncruces/go-sqlite3)
wraps the [C SQLite API](https://www.sqlite.org/cintro.html)
([example usage](https://pkg.go.dev/github.com/ncruces/go-sqlite3#example-package)).
- Package [`github.com/ncruces/go-sqlite3/driver`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/driver)
provides a [`database/sql`](https://pkg.go.dev/database/sql) driver
([example usage](https://pkg.go.dev/github.com/ncruces/go-sqlite3/driver#example-package)).
- Package [`github.com/ncruces/go-sqlite3/embed`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/embed)
embeds a build of SQLite into your application.
### Caveats
This module replaces the SQLite [OS Interface](https://www.sqlite.org/vfs.html) (aka VFS)
with a pure Go implementation.
This has numerous benefits, but also comes with some drawbacks.
#### Write-Ahead Logging
Because WASM does not support shared memory,
[WAL](https://www.sqlite.org/wal.html) support is [limited](https://www.sqlite.org/wal.html#noshm).
To work around this limitation, SQLite is compiled with
[`SQLITE_DEFAULT_LOCKING_MODE=1`](https://www.sqlite.org/compile.html#default_locking_mode),
making `EXCLUSIVE` the default locking mode.
For non-WAL databases, `NORMAL` locking mode can be activated with
[`PRAGMA locking_mode=NORMAL`](https://www.sqlite.org/pragma.html#pragma_locking_mode).
Because connection pooling is incompatible with `EXCLUSIVE` locking mode,
the `database/sql` driver defaults to `NORMAL` locking mode.
To open WAL databases, or use `EXCLUSIVE` locking mode,
disable connection pooling by calling
[`db.SetMaxOpenConns(1)`](https://pkg.go.dev/database/sql#DB.SetMaxOpenConns).
#### POSIX Advisory Locks
POSIX advisory locks, which SQLite uses, are
[broken by design](https://www.sqlite.org/src/artifact/90c4fa?ln=1073-1161).
On Linux, macOS and illumos, this module uses
[OFD locks](https://www.gnu.org/software/libc/manual/html_node/Open-File-Description-Locks.html)
to synchronize access to database files.
OFD locks are fully compatible with process-associated POSIX advisory locks.
On BSD Unixes, this module uses
[BSD locks](https://man.freebsd.org/cgi/man.cgi?query=flock&sektion=2).
BSD locks may _not_ be compatible with process-associated POSIX advisory locks.
#### Testing
The pure Go VFS is stress tested by running an unmodified build of SQLite's
[mptest](https://github.com/sqlite/sqlite/blob/master/mptest/mptest.c)
on Linux, macOS and Windows.
Performance is tested by running
[speedtest1](https://github.com/sqlite/sqlite/blob/master/test/speedtest1.c).
### Roadmap
- [ ] advanced SQLite features
- [x] nested transactions
- [x] incremental BLOB I/O
- [x] online backup
- [ ] session extension
- [ ] custom SQL functions
- [ ] custom VFSes
- [ ] in-memory VFS
- [ ] read-only VFS, wrapping an [`io.ReaderAt`](https://pkg.go.dev/io#ReaderAt)
- [ ] cloud-based VFS, based on [Cloud Backed SQLite](https://sqlite.org/cloudsqlite/doc/trunk/www/index.wiki)
- [ ] custom VFS API
### Alternatives
- [`modernc.org/sqlite`](https://pkg.go.dev/modernc.org/sqlite)
- [`crawshaw.io/sqlite`](https://pkg.go.dev/crawshaw.io/sqlite)
- [`github.com/mattn/go-sqlite3`](https://pkg.go.dev/github.com/mattn/go-sqlite3)
- [`github.com/zombiezen/go-sqlite`](https://pkg.go.dev/github.com/zombiezen/go-sqlite)
+134
View File
@@ -0,0 +1,134 @@
package sqlite3
// Backup is a handle to an open BLOB.
//
// https://www.sqlite.org/c3ref/backup.html
type Backup struct {
c *Conn
handle uint32
otherc uint32
}
// Backup backs up srcDB on the src connection to the "main" database in dstURI.
//
// Backup calls [Open] to open the SQLite database file dstURI,
// and blocks until the entire backup is complete.
// Use [Conn.BackupInit] for incremental backup.
//
// https://www.sqlite.org/backup.html
func (src *Conn) Backup(srcDB, dstURI string) error {
b, err := src.BackupInit(srcDB, dstURI)
if err != nil {
return err
}
defer b.Close()
_, err = b.Step(-1)
return err
}
// Restore restores dstDB on the dst connection from the "main" database in srcURI.
//
// Restore calls [Open] to open the SQLite database file srcURI,
// and blocks until the entire restore is complete.
//
// https://www.sqlite.org/backup.html
func (dst *Conn) Restore(dstDB, srcURI string) error {
src, err := dst.openDB(srcURI, OPEN_READONLY|OPEN_URI)
if err != nil {
return err
}
b, err := dst.backupInit(dst.handle, dstDB, src, "main")
if err != nil {
return err
}
defer b.Close()
_, err = b.Step(-1)
return err
}
// BackupInit initializes a backup operation to copy the content of one database into another.
//
// BackupInit calls [Open] to open the SQLite database file dstURI,
// then initializes a backup that copies the contents of srcDB on the src connection
// to the "main" database in dstURI.
//
// https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupinit
func (src *Conn) BackupInit(srcDB, dstURI string) (*Backup, error) {
dst, err := src.openDB(dstURI, OPEN_READWRITE|OPEN_CREATE|OPEN_URI)
if err != nil {
return nil, err
}
return src.backupInit(dst, "main", src.handle, srcDB)
}
func (c *Conn) backupInit(dst uint32, dstName string, src uint32, srcName string) (*Backup, error) {
defer c.arena.reset()
dstPtr := c.arena.string(dstName)
srcPtr := c.arena.string(srcName)
other := dst
if c.handle == dst {
other = src
}
r := c.call(c.api.backupInit,
uint64(dst), uint64(dstPtr),
uint64(src), uint64(srcPtr))
if r[0] == 0 {
defer c.closeDB(other)
r = c.call(c.api.errcode, uint64(dst))
return nil, c.module.error(r[0], dst)
}
return &Backup{
c: c,
otherc: other,
handle: uint32(r[0]),
}, nil
}
// Close finishes a backup operation.
//
// It is safe to close a nil, zero or closed Backup.
//
// https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupfinish
func (b *Backup) Close() error {
if b == nil || b.handle == 0 {
return nil
}
r := b.c.call(b.c.api.backupFinish, uint64(b.handle))
b.c.closeDB(b.otherc)
b.handle = 0
return b.c.error(r[0])
}
// Step copies up to nPage pages between the source and destination databases.
// If nPage is negative, all remaining source pages are copied.
//
// https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupstep
func (b *Backup) Step(nPage int) (done bool, err error) {
r := b.c.call(b.c.api.backupStep, uint64(b.handle), uint64(nPage))
if r[0] == _DONE {
return true, nil
}
return false, b.c.error(r[0])
}
// Remaining returns the number of pages still to be backed up
// at the conclusion of the most recent [Backup.Step].
//
// https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupremaining
func (b *Backup) Remaining() int {
r := b.c.call(b.c.api.backupRemaining, uint64(b.handle))
return int(r[0])
}
// PageCount returns the total number of pages in the source database
// at the conclusion of the most recent [Backup.Step].
//
// https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backuppagecount
func (b *Backup) PageCount() int {
r := b.c.call(b.c.api.backupFinish, uint64(b.handle))
return int(r[0])
}
+159
View File
@@ -0,0 +1,159 @@
package sqlite3
import "io"
// ZeroBlob represents a zero-filled, length n BLOB
// that can be used as an argument to
// [database/sql.DB.Exec] and similar methods.
type ZeroBlob int64
// Blob is a handle to an open BLOB.
//
// It implements [io.ReadWriteSeeker] for incremental BLOB I/O.
//
// https://www.sqlite.org/c3ref/blob.html
type Blob struct {
c *Conn
handle uint32
bytes int64
offset int64
}
var _ io.ReadWriteSeeker = &Blob{}
// OpenBlob opens a BLOB for incremental I/O.
//
// https://www.sqlite.org/c3ref/blob_open.html
func (c *Conn) OpenBlob(db, table, column string, row int64, write bool) (*Blob, error) {
c.checkInterrupt()
defer c.arena.reset()
blobPtr := c.arena.new(ptrlen)
dbPtr := c.arena.string(db)
tablePtr := c.arena.string(table)
columnPtr := c.arena.string(column)
var flags uint64
if write {
flags = 1
}
r := c.call(c.api.blobOpen, uint64(c.handle),
uint64(dbPtr), uint64(tablePtr), uint64(columnPtr),
uint64(row), flags, uint64(blobPtr))
if err := c.error(r[0]); err != nil {
return nil, err
}
blob := Blob{c: c}
blob.handle = c.mem.readUint32(blobPtr)
blob.bytes = int64(c.call(c.api.blobBytes, uint64(blob.handle))[0])
return &blob, nil
}
// Close closes a BLOB handle.
//
// It is safe to close a nil, zero or closed Blob.
//
// https://www.sqlite.org/c3ref/blob_close.html
func (b *Blob) Close() error {
if b == nil || b.handle == 0 {
return nil
}
r := b.c.call(b.c.api.blobClose, uint64(b.handle))
b.handle = 0
return b.c.error(r[0])
}
// Size returns the size of the BLOB in bytes.
//
// https://www.sqlite.org/c3ref/blob_bytes.html
func (b *Blob) Size() int64 {
return b.bytes
}
// Read implements the [io.Reader] interface.
//
// https://www.sqlite.org/c3ref/blob_read.html
func (b *Blob) Read(p []byte) (n int, err error) {
if b.offset >= b.bytes {
return 0, io.EOF
}
want := int64(len(p))
avail := b.bytes - b.offset
if want > avail {
want = avail
}
ptr := b.c.new(uint64(want))
defer b.c.free(ptr)
r := b.c.call(b.c.api.blobRead, uint64(b.handle),
uint64(ptr), uint64(want), uint64(b.offset))
err = b.c.error(r[0])
if err != nil {
return 0, err
}
mem := b.c.mem.view(ptr, uint64(want))
copy(p, mem)
b.offset += want
if b.offset >= b.bytes {
err = io.EOF
}
return int(want), err
}
// Write implements the [io.Writer] interface.
//
// https://www.sqlite.org/c3ref/blob_write.html
func (b *Blob) Write(p []byte) (n int, err error) {
offset := b.offset
if offset > b.bytes {
offset = b.bytes
}
ptr := b.c.newBytes(p)
defer b.c.free(ptr)
r := b.c.call(b.c.api.blobWrite, uint64(b.handle),
uint64(ptr), uint64(len(p)), uint64(offset))
err = b.c.error(r[0])
if err != nil {
return 0, err
}
b.offset += int64(len(p))
return len(p), nil
}
// Seek implements the [io.Seeker] interface.
func (b *Blob) Seek(offset int64, whence int) (int64, error) {
switch whence {
default:
return 0, whenceErr
case io.SeekStart:
break
case io.SeekCurrent:
offset += b.offset
case io.SeekEnd:
offset += b.bytes
}
if offset < 0 {
return 0, offsetErr
}
b.offset = offset
return offset, nil
}
// Reopen moves a BLOB handle to a new row of the same database table.
//
// https://www.sqlite.org/c3ref/blob_reopen.html
func (b *Blob) Reopen(row int64) error {
r := b.c.call(b.c.api.blobReopen, uint64(b.handle), uint64(row))
b.bytes = int64(b.c.call(b.c.api.blobBytes, uint64(b.handle))[0])
b.offset = 0
return b.c.error(r[0])
}
+334
View File
@@ -0,0 +1,334 @@
package sqlite3
import (
"context"
"database/sql/driver"
"errors"
"fmt"
"net/url"
"runtime"
"strings"
"sync/atomic"
"unsafe"
)
// Conn is a database connection handle.
// A Conn is not safe for concurrent use by multiple goroutines.
//
// https://www.sqlite.org/c3ref/sqlite3.html
type Conn struct {
*module
handle uint32
arena arena
interrupt context.Context
waiter chan struct{}
pending *Stmt
}
// Open calls [OpenFlags] with [OPEN_READWRITE], [OPEN_CREATE], [OPEN_URI] and [OPEN_NOFOLLOW].
func Open(filename string) (*Conn, error) {
return newConn(filename, OPEN_READWRITE|OPEN_CREATE|OPEN_URI|OPEN_NOFOLLOW)
}
// OpenFlags opens an SQLite database file as specified by the filename argument.
//
// If none of the required flags is used, a combination of [OPEN_READWRITE] and [OPEN_CREATE] is used.
// If a URI filename is used, PRAGMA statements to execute can be specified using "_pragma":
//
// sqlite3.Open("file:demo.db?_pragma=busy_timeout(10000)&_pragma=locking_mode(normal)")
//
// https://www.sqlite.org/c3ref/open.html
func OpenFlags(filename string, flags OpenFlag) (*Conn, error) {
if flags&(OPEN_READONLY|OPEN_READWRITE|OPEN_CREATE) == 0 {
flags |= OPEN_READWRITE | OPEN_CREATE
}
return newConn(filename, flags)
}
func newConn(filename string, flags OpenFlag) (conn *Conn, err error) {
mod, err := instantiateModule()
if err != nil {
return nil, err
}
defer func() {
if conn == nil {
mod.close()
} else {
runtime.SetFinalizer(conn, finalizer[Conn](3))
}
}()
c := &Conn{module: mod}
c.arena = c.newArena(1024)
c.handle, err = c.openDB(filename, flags)
if err != nil {
return nil, err
}
return c, nil
}
func (c *Conn) openDB(filename string, flags OpenFlag) (uint32, error) {
defer c.arena.reset()
connPtr := c.arena.new(ptrlen)
namePtr := c.arena.string(filename)
flags |= OPEN_EXRESCODE
r := c.call(c.api.open, uint64(namePtr), uint64(connPtr), uint64(flags), 0)
handle := c.mem.readUint32(connPtr)
if err := c.module.error(r[0], handle); err != nil {
c.closeDB(handle)
return 0, err
}
if flags|OPEN_URI != 0 && strings.HasPrefix(filename, "file:") {
var pragmas strings.Builder
if _, after, ok := strings.Cut(filename, "?"); ok {
query, _ := url.ParseQuery(after)
for _, p := range query["_pragma"] {
pragmas.WriteString(`PRAGMA `)
pragmas.WriteString(p)
pragmas.WriteByte(';')
}
}
c.arena.reset()
pragmaPtr := c.arena.string(pragmas.String())
r := c.call(c.api.exec, uint64(handle), uint64(pragmaPtr), 0, 0, 0)
if err := c.module.error(r[0], handle, pragmas.String()); err != nil {
if errors.Is(err, ERROR) {
err = fmt.Errorf("sqlite3: invalid _pragma: %w", err)
}
c.closeDB(handle)
return 0, err
}
}
return handle, nil
}
func (c *Conn) closeDB(handle uint32) {
r := c.call(c.api.closeZombie, uint64(handle))
if err := c.module.error(r[0], handle); err != nil {
panic(err)
}
}
// Close closes the database connection.
//
// If the database connection is associated with unfinalized prepared statements,
// open blob handles, and/or unfinished backup objects,
// Close will leave the database connection open and return [BUSY].
//
// It is safe to close a nil, zero or closed Conn.
//
// https://www.sqlite.org/c3ref/close.html
func (c *Conn) Close() error {
if c == nil || c.handle == 0 {
return nil
}
c.SetInterrupt(context.Background())
c.pending.Close()
c.pending = nil
r := c.call(c.api.close, uint64(c.handle))
if err := c.error(r[0]); err != nil {
return err
}
c.handle = 0
runtime.SetFinalizer(c, nil)
return c.module.close()
}
// Exec is a convenience function that allows an application to run
// multiple statements of SQL without having to use a lot of code.
//
// https://www.sqlite.org/c3ref/exec.html
func (c *Conn) Exec(sql string) error {
c.checkInterrupt()
defer c.arena.reset()
sqlPtr := c.arena.string(sql)
r := c.call(c.api.exec, uint64(c.handle), uint64(sqlPtr), 0, 0, 0)
return c.error(r[0])
}
// Prepare calls [Conn.PrepareFlags] with no flags.
func (c *Conn) Prepare(sql string) (stmt *Stmt, tail string, err error) {
return c.PrepareFlags(sql, 0)
}
// PrepareFlags compiles the first SQL statement in sql;
// tail is left pointing to what remains uncompiled.
// If the input text contains no SQL (if the input is an empty string or a comment),
// both stmt and err will be nil.
//
// https://www.sqlite.org/c3ref/prepare.html
func (c *Conn) PrepareFlags(sql string, flags PrepareFlag) (stmt *Stmt, tail string, err error) {
if emptyStatement(sql) {
return nil, "", nil
}
defer c.arena.reset()
stmtPtr := c.arena.new(ptrlen)
tailPtr := c.arena.new(ptrlen)
sqlPtr := c.arena.string(sql)
r := c.call(c.api.prepare, uint64(c.handle),
uint64(sqlPtr), uint64(len(sql)+1), uint64(flags),
uint64(stmtPtr), uint64(tailPtr))
stmt = &Stmt{c: c}
stmt.handle = c.mem.readUint32(stmtPtr)
i := c.mem.readUint32(tailPtr)
tail = sql[i-sqlPtr:]
if err := c.error(r[0], sql); err != nil {
return nil, "", err
}
if stmt.handle == 0 {
return nil, "", nil
}
return
}
// GetAutocommit tests the connection for auto-commit mode.
//
// https://www.sqlite.org/c3ref/get_autocommit.html
func (c *Conn) GetAutocommit() bool {
r := c.call(c.api.autocommit, uint64(c.handle))
return r[0] != 0
}
// LastInsertRowID returns the rowid of the most recent successful INSERT
// on the database connection.
//
// https://www.sqlite.org/c3ref/last_insert_rowid.html
func (c *Conn) LastInsertRowID() int64 {
r := c.call(c.api.lastRowid, uint64(c.handle))
return int64(r[0])
}
// Changes returns the number of rows modified, inserted or deleted
// by the most recently completed INSERT, UPDATE or DELETE statement
// on the database connection.
//
// https://www.sqlite.org/c3ref/changes.html
func (c *Conn) Changes() int64 {
r := c.call(c.api.changes, uint64(c.handle))
return int64(r[0])
}
// SetInterrupt interrupts a long-running query when a context is done.
//
// Subsequent uses of the connection will return [INTERRUPT]
// until the context is reset by another call to SetInterrupt.
//
// To associate a timeout with a connection:
//
// ctx, cancel := context.WithTimeout(context.TODO(), 100*time.Millisecond)
// conn.SetInterrupt(ctx)
// defer cancel()
//
// SetInterrupt returns the old context assigned to the connection.
//
// https://www.sqlite.org/c3ref/interrupt.html
func (c *Conn) SetInterrupt(ctx context.Context) (old context.Context) {
// Is a waiter running?
if c.waiter != nil {
c.waiter <- struct{}{} // Cancel the waiter.
<-c.waiter // Wait for it to finish.
c.waiter = nil
}
// Reset the pending statement.
if c.pending != nil {
c.pending.Reset()
}
old = c.interrupt
c.interrupt = ctx
if ctx == nil || ctx.Done() == nil {
return old
}
// Creating an uncompleted SQL statement prevents SQLite from ignoring
// an interrupt that comes before any other statements are started.
if c.pending == nil {
c.pending, _, _ = c.Prepare(`SELECT 1 UNION ALL SELECT 2`)
}
c.pending.Step()
// Don't create the goroutine if we're already interrupted.
// This happens frequently while restoring to a previously interrupted state.
if c.checkInterrupt() {
return old
}
waiter := make(chan struct{})
c.waiter = waiter
go func() {
select {
case <-waiter: // Waiter was cancelled.
break
case <-ctx.Done(): // Done was closed.
buf := c.mem.view(c.handle+c.api.interrupt, 4)
(*atomic.Uint32)(unsafe.Pointer(&buf[0])).Store(1)
// Wait for the next call to SetInterrupt.
<-waiter
}
// Signal that the waiter has finished.
waiter <- struct{}{}
}()
return old
}
func (c *Conn) checkInterrupt() bool {
if c.interrupt == nil || c.interrupt.Err() == nil {
return false
}
buf := c.mem.view(c.handle+c.api.interrupt, 4)
(*atomic.Uint32)(unsafe.Pointer(&buf[0])).Store(1)
return true
}
// Pragma executes a PRAGMA statement and returns any results.
//
// https://www.sqlite.org/pragma.html
func (c *Conn) Pragma(str string) ([]string, error) {
stmt, _, err := c.Prepare(`PRAGMA ` + str)
if err != nil {
return nil, err
}
defer stmt.Close()
var pragmas []string
for stmt.Step() {
pragmas = append(pragmas, stmt.ColumnText(0))
}
return pragmas, stmt.Close()
}
func (c *Conn) error(rc uint64, sql ...string) error {
return c.module.error(rc, c.handle, sql...)
}
// DriverConn is implemented by the SQLite [database/sql] driver connection.
//
// It can be used to access advanced SQLite features like
// [savepoints] and [incremental BLOB I/O].
//
// [savepoints]: https://www.sqlite.org/lang_savepoint.html
// [incremental BLOB I/O]: https://www.sqlite.org/c3ref/blob_open.html
type DriverConn interface {
driver.ConnBeginTx
driver.ExecerContext
driver.ConnPrepareContext
Savepoint() Savepoint
OpenBlob(db, table, column string, row int64, write bool) (*Blob, error)
}
+275
View File
@@ -0,0 +1,275 @@
package sqlite3
import "strconv"
const (
_OK = 0 /* Successful result */
_ROW = 100 /* sqlite3_step() has another row ready */
_DONE = 101 /* sqlite3_step() has finished executing */
_OK_SYMLINK = (_OK | (2 << 8)) /* internal use only */
_UTF8 = 1
_MAX_STRING = 512 // Used for short strings: names, error messages…
_MAX_PATHNAME = 512
_MAX_ALLOCATION_SIZE = 0x7ffffeff
ptrlen = 4
)
// ErrorCode is a result code that [Error.Code] might return.
//
// https://www.sqlite.org/rescode.html
type ErrorCode uint8
const (
ERROR ErrorCode = 1 /* Generic error */
INTERNAL ErrorCode = 2 /* Internal logic error in SQLite */
PERM ErrorCode = 3 /* Access permission denied */
ABORT ErrorCode = 4 /* Callback routine requested an abort */
BUSY ErrorCode = 5 /* The database file is locked */
LOCKED ErrorCode = 6 /* A table in the database is locked */
NOMEM ErrorCode = 7 /* A malloc() failed */
READONLY ErrorCode = 8 /* Attempt to write a readonly database */
INTERRUPT ErrorCode = 9 /* Operation terminated by sqlite3_interrupt() */
IOERR ErrorCode = 10 /* Some kind of disk I/O error occurred */
CORRUPT ErrorCode = 11 /* The database disk image is malformed */
NOTFOUND ErrorCode = 12 /* Unknown opcode in sqlite3_file_control() */
FULL ErrorCode = 13 /* Insertion failed because database is full */
CANTOPEN ErrorCode = 14 /* Unable to open the database file */
PROTOCOL ErrorCode = 15 /* Database lock protocol error */
EMPTY ErrorCode = 16 /* Internal use only */
SCHEMA ErrorCode = 17 /* The database schema changed */
TOOBIG ErrorCode = 18 /* String or BLOB exceeds size limit */
CONSTRAINT ErrorCode = 19 /* Abort due to constraint violation */
MISMATCH ErrorCode = 20 /* Data type mismatch */
MISUSE ErrorCode = 21 /* Library used incorrectly */
NOLFS ErrorCode = 22 /* Uses OS features not supported on host */
AUTH ErrorCode = 23 /* Authorization denied */
FORMAT ErrorCode = 24 /* Not used */
RANGE ErrorCode = 25 /* 2nd parameter to sqlite3_bind out of range */
NOTADB ErrorCode = 26 /* File opened that is not a database file */
NOTICE ErrorCode = 27 /* Notifications from sqlite3_log() */
WARNING ErrorCode = 28 /* Warnings from sqlite3_log() */
)
// ExtendedErrorCode is a result code that [Error.ExtendedCode] might return.
//
// https://www.sqlite.org/rescode.html
type (
ExtendedErrorCode uint16
xErrorCode = ExtendedErrorCode
)
const (
ERROR_MISSING_COLLSEQ ExtendedErrorCode = xErrorCode(ERROR) | (1 << 8)
ERROR_RETRY ExtendedErrorCode = xErrorCode(ERROR) | (2 << 8)
ERROR_SNAPSHOT ExtendedErrorCode = xErrorCode(ERROR) | (3 << 8)
IOERR_READ ExtendedErrorCode = xErrorCode(IOERR) | (1 << 8)
IOERR_SHORT_READ ExtendedErrorCode = xErrorCode(IOERR) | (2 << 8)
IOERR_WRITE ExtendedErrorCode = xErrorCode(IOERR) | (3 << 8)
IOERR_FSYNC ExtendedErrorCode = xErrorCode(IOERR) | (4 << 8)
IOERR_DIR_FSYNC ExtendedErrorCode = xErrorCode(IOERR) | (5 << 8)
IOERR_TRUNCATE ExtendedErrorCode = xErrorCode(IOERR) | (6 << 8)
IOERR_FSTAT ExtendedErrorCode = xErrorCode(IOERR) | (7 << 8)
IOERR_UNLOCK ExtendedErrorCode = xErrorCode(IOERR) | (8 << 8)
IOERR_RDLOCK ExtendedErrorCode = xErrorCode(IOERR) | (9 << 8)
IOERR_DELETE ExtendedErrorCode = xErrorCode(IOERR) | (10 << 8)
IOERR_BLOCKED ExtendedErrorCode = xErrorCode(IOERR) | (11 << 8)
IOERR_NOMEM ExtendedErrorCode = xErrorCode(IOERR) | (12 << 8)
IOERR_ACCESS ExtendedErrorCode = xErrorCode(IOERR) | (13 << 8)
IOERR_CHECKRESERVEDLOCK ExtendedErrorCode = xErrorCode(IOERR) | (14 << 8)
IOERR_LOCK ExtendedErrorCode = xErrorCode(IOERR) | (15 << 8)
IOERR_CLOSE ExtendedErrorCode = xErrorCode(IOERR) | (16 << 8)
IOERR_DIR_CLOSE ExtendedErrorCode = xErrorCode(IOERR) | (17 << 8)
IOERR_SHMOPEN ExtendedErrorCode = xErrorCode(IOERR) | (18 << 8)
IOERR_SHMSIZE ExtendedErrorCode = xErrorCode(IOERR) | (19 << 8)
IOERR_SHMLOCK ExtendedErrorCode = xErrorCode(IOERR) | (20 << 8)
IOERR_SHMMAP ExtendedErrorCode = xErrorCode(IOERR) | (21 << 8)
IOERR_SEEK ExtendedErrorCode = xErrorCode(IOERR) | (22 << 8)
IOERR_DELETE_NOENT ExtendedErrorCode = xErrorCode(IOERR) | (23 << 8)
IOERR_MMAP ExtendedErrorCode = xErrorCode(IOERR) | (24 << 8)
IOERR_GETTEMPPATH ExtendedErrorCode = xErrorCode(IOERR) | (25 << 8)
IOERR_CONVPATH ExtendedErrorCode = xErrorCode(IOERR) | (26 << 8)
IOERR_VNODE ExtendedErrorCode = xErrorCode(IOERR) | (27 << 8)
IOERR_AUTH ExtendedErrorCode = xErrorCode(IOERR) | (28 << 8)
IOERR_BEGIN_ATOMIC ExtendedErrorCode = xErrorCode(IOERR) | (29 << 8)
IOERR_COMMIT_ATOMIC ExtendedErrorCode = xErrorCode(IOERR) | (30 << 8)
IOERR_ROLLBACK_ATOMIC ExtendedErrorCode = xErrorCode(IOERR) | (31 << 8)
IOERR_DATA ExtendedErrorCode = xErrorCode(IOERR) | (32 << 8)
IOERR_CORRUPTFS ExtendedErrorCode = xErrorCode(IOERR) | (33 << 8)
LOCKED_SHAREDCACHE ExtendedErrorCode = xErrorCode(LOCKED) | (1 << 8)
LOCKED_VTAB ExtendedErrorCode = xErrorCode(LOCKED) | (2 << 8)
BUSY_RECOVERY ExtendedErrorCode = xErrorCode(BUSY) | (1 << 8)
BUSY_SNAPSHOT ExtendedErrorCode = xErrorCode(BUSY) | (2 << 8)
BUSY_TIMEOUT ExtendedErrorCode = xErrorCode(BUSY) | (3 << 8)
CANTOPEN_NOTEMPDIR ExtendedErrorCode = xErrorCode(CANTOPEN) | (1 << 8)
CANTOPEN_ISDIR ExtendedErrorCode = xErrorCode(CANTOPEN) | (2 << 8)
CANTOPEN_FULLPATH ExtendedErrorCode = xErrorCode(CANTOPEN) | (3 << 8)
CANTOPEN_CONVPATH ExtendedErrorCode = xErrorCode(CANTOPEN) | (4 << 8)
CANTOPEN_DIRTYWAL ExtendedErrorCode = xErrorCode(CANTOPEN) | (5 << 8) /* Not Used */
CANTOPEN_SYMLINK ExtendedErrorCode = xErrorCode(CANTOPEN) | (6 << 8)
CORRUPT_VTAB ExtendedErrorCode = xErrorCode(CORRUPT) | (1 << 8)
CORRUPT_SEQUENCE ExtendedErrorCode = xErrorCode(CORRUPT) | (2 << 8)
CORRUPT_INDEX ExtendedErrorCode = xErrorCode(CORRUPT) | (3 << 8)
READONLY_RECOVERY ExtendedErrorCode = xErrorCode(READONLY) | (1 << 8)
READONLY_CANTLOCK ExtendedErrorCode = xErrorCode(READONLY) | (2 << 8)
READONLY_ROLLBACK ExtendedErrorCode = xErrorCode(READONLY) | (3 << 8)
READONLY_DBMOVED ExtendedErrorCode = xErrorCode(READONLY) | (4 << 8)
READONLY_CANTINIT ExtendedErrorCode = xErrorCode(READONLY) | (5 << 8)
READONLY_DIRECTORY ExtendedErrorCode = xErrorCode(READONLY) | (6 << 8)
ABORT_ROLLBACK ExtendedErrorCode = xErrorCode(ABORT) | (2 << 8)
CONSTRAINT_CHECK ExtendedErrorCode = xErrorCode(CONSTRAINT) | (1 << 8)
CONSTRAINT_COMMITHOOK ExtendedErrorCode = xErrorCode(CONSTRAINT) | (2 << 8)
CONSTRAINT_FOREIGNKEY ExtendedErrorCode = xErrorCode(CONSTRAINT) | (3 << 8)
CONSTRAINT_FUNCTION ExtendedErrorCode = xErrorCode(CONSTRAINT) | (4 << 8)
CONSTRAINT_NOTNULL ExtendedErrorCode = xErrorCode(CONSTRAINT) | (5 << 8)
CONSTRAINT_PRIMARYKEY ExtendedErrorCode = xErrorCode(CONSTRAINT) | (6 << 8)
CONSTRAINT_TRIGGER ExtendedErrorCode = xErrorCode(CONSTRAINT) | (7 << 8)
CONSTRAINT_UNIQUE ExtendedErrorCode = xErrorCode(CONSTRAINT) | (8 << 8)
CONSTRAINT_VTAB ExtendedErrorCode = xErrorCode(CONSTRAINT) | (9 << 8)
CONSTRAINT_ROWID ExtendedErrorCode = xErrorCode(CONSTRAINT) | (10 << 8)
CONSTRAINT_PINNED ExtendedErrorCode = xErrorCode(CONSTRAINT) | (11 << 8)
CONSTRAINT_DATATYPE ExtendedErrorCode = xErrorCode(CONSTRAINT) | (12 << 8)
NOTICE_RECOVER_WAL ExtendedErrorCode = xErrorCode(NOTICE) | (1 << 8)
NOTICE_RECOVER_ROLLBACK ExtendedErrorCode = xErrorCode(NOTICE) | (2 << 8)
NOTICE_RBU ExtendedErrorCode = xErrorCode(NOTICE) | (3 << 8)
WARNING_AUTOINDEX ExtendedErrorCode = xErrorCode(WARNING) | (1 << 8)
AUTH_USER ExtendedErrorCode = xErrorCode(AUTH) | (1 << 8)
)
// OpenFlag is a flag for a file open operation.
//
// https://www.sqlite.org/c3ref/c_open_autoproxy.html
type OpenFlag uint32
const (
OPEN_READONLY OpenFlag = 0x00000001 /* Ok for sqlite3_open_v2() */
OPEN_READWRITE OpenFlag = 0x00000002 /* Ok for sqlite3_open_v2() */
OPEN_CREATE OpenFlag = 0x00000004 /* Ok for sqlite3_open_v2() */
OPEN_DELETEONCLOSE OpenFlag = 0x00000008 /* VFS only */
OPEN_EXCLUSIVE OpenFlag = 0x00000010 /* VFS only */
OPEN_AUTOPROXY OpenFlag = 0x00000020 /* VFS only */
OPEN_URI OpenFlag = 0x00000040 /* Ok for sqlite3_open_v2() */
OPEN_MEMORY OpenFlag = 0x00000080 /* Ok for sqlite3_open_v2() */
OPEN_MAIN_DB OpenFlag = 0x00000100 /* VFS only */
OPEN_TEMP_DB OpenFlag = 0x00000200 /* VFS only */
OPEN_TRANSIENT_DB OpenFlag = 0x00000400 /* VFS only */
OPEN_MAIN_JOURNAL OpenFlag = 0x00000800 /* VFS only */
OPEN_TEMP_JOURNAL OpenFlag = 0x00001000 /* VFS only */
OPEN_SUBJOURNAL OpenFlag = 0x00002000 /* VFS only */
OPEN_SUPER_JOURNAL OpenFlag = 0x00004000 /* VFS only */
OPEN_NOMUTEX OpenFlag = 0x00008000 /* Ok for sqlite3_open_v2() */
OPEN_FULLMUTEX OpenFlag = 0x00010000 /* Ok for sqlite3_open_v2() */
OPEN_SHAREDCACHE OpenFlag = 0x00020000 /* Ok for sqlite3_open_v2() */
OPEN_PRIVATECACHE OpenFlag = 0x00040000 /* Ok for sqlite3_open_v2() */
OPEN_WAL OpenFlag = 0x00080000 /* VFS only */
OPEN_NOFOLLOW OpenFlag = 0x01000000 /* Ok for sqlite3_open_v2() */
OPEN_EXRESCODE OpenFlag = 0x02000000 /* Extended result codes */
)
// PrepareFlag is a flag that can be passed to [Conn.PrepareFlags].
//
// https://www.sqlite.org/c3ref/c_prepare_normalize.html
type PrepareFlag uint32
const (
PREPARE_PERSISTENT PrepareFlag = 0x01
PREPARE_NORMALIZE PrepareFlag = 0x02
PREPARE_NO_VTAB PrepareFlag = 0x04
)
// Datatype is a fundamental datatype of SQLite.
//
// https://www.sqlite.org/c3ref/c_blob.html
type Datatype uint32
const (
INTEGER Datatype = 1
FLOAT Datatype = 2
TEXT Datatype = 3
BLOB Datatype = 4
NULL Datatype = 5
)
// String implements the [fmt.Stringer] interface.
func (t Datatype) String() string {
const name = "INTEGERFLOATTEXTBLOBNULL"
switch t {
case INTEGER:
return name[0:7]
case FLOAT:
return name[7:12]
case TEXT:
return name[12:16]
case BLOB:
return name[16:20]
case NULL:
return name[20:24]
}
return strconv.FormatUint(uint64(t), 10)
}
type _AccessFlag uint32
const (
_ACCESS_EXISTS _AccessFlag = 0
_ACCESS_READWRITE _AccessFlag = 1 /* Used by PRAGMA temp_store_directory */
_ACCESS_READ _AccessFlag = 2 /* Unused */
)
type _SyncFlag uint32
const (
_SYNC_NORMAL _SyncFlag = 0x00002
_SYNC_FULL _SyncFlag = 0x00003
_SYNC_DATAONLY _SyncFlag = 0x00010
)
type _FcntlOpcode uint32
const (
_FCNTL_LOCKSTATE = 1
_FCNTL_GET_LOCKPROXYFILE = 2
_FCNTL_SET_LOCKPROXYFILE = 3
_FCNTL_LAST_ERRNO = 4
_FCNTL_SIZE_HINT = 5
_FCNTL_CHUNK_SIZE = 6
_FCNTL_FILE_POINTER = 7
_FCNTL_SYNC_OMITTED = 8
_FCNTL_WIN32_AV_RETRY = 9
_FCNTL_PERSIST_WAL = 10
_FCNTL_OVERWRITE = 11
_FCNTL_VFSNAME = 12
_FCNTL_POWERSAFE_OVERWRITE = 13
_FCNTL_PRAGMA = 14
_FCNTL_BUSYHANDLER = 15
_FCNTL_TEMPFILENAME = 16
_FCNTL_MMAP_SIZE = 18
_FCNTL_TRACE = 19
_FCNTL_HAS_MOVED = 20
_FCNTL_SYNC = 21
_FCNTL_COMMIT_PHASETWO = 22
_FCNTL_WIN32_SET_HANDLE = 23
_FCNTL_WAL_BLOCK = 24
_FCNTL_ZIPVFS = 25
_FCNTL_RBU = 26
_FCNTL_VFS_POINTER = 27
_FCNTL_JOURNAL_POINTER = 28
_FCNTL_WIN32_GET_HANDLE = 29
_FCNTL_PDB = 30
_FCNTL_BEGIN_ATOMIC_WRITE = 31
_FCNTL_COMMIT_ATOMIC_WRITE = 32
_FCNTL_ROLLBACK_ATOMIC_WRITE = 33
_FCNTL_LOCK_TIMEOUT = 34
_FCNTL_DATA_VERSION = 35
_FCNTL_SIZE_LIMIT = 36
_FCNTL_CKPT_DONE = 37
_FCNTL_RESERVE_BYTES = 38
_FCNTL_CKPT_START = 39
_FCNTL_EXTERNAL_READER = 40
_FCNTL_CKSM_FILE = 41
_FCNTL_RESET_CACHE = 42
)
+223
View File
@@ -0,0 +1,223 @@
package sqlite3
import (
"fmt"
"runtime"
"strconv"
"strings"
)
// Error wraps an SQLite Error Code.
//
// https://www.sqlite.org/c3ref/errcode.html
type Error struct {
code uint64
str string
msg string
sql string
}
// Code returns the primary error code for this error.
//
// https://www.sqlite.org/rescode.html
func (e *Error) Code() ErrorCode {
return ErrorCode(e.code)
}
// ExtendedCode returns the extended error code for this error.
//
// https://www.sqlite.org/rescode.html
func (e *Error) ExtendedCode() ExtendedErrorCode {
return ExtendedErrorCode(e.code)
}
// Error implements the error interface.
func (e *Error) Error() string {
var b strings.Builder
b.WriteString("sqlite3: ")
if e.str != "" {
b.WriteString(e.str)
} else {
b.WriteString(strconv.Itoa(int(e.code)))
}
if e.msg != "" {
b.WriteByte(':')
b.WriteByte(' ')
b.WriteString(e.msg)
}
return b.String()
}
// Is tests whether this error matches a given [ErrorCode] or [ExtendedErrorCode].
//
// It makes it possible to do:
//
// if errors.Is(err, sqlite3.BUSY) {
// // ... handle BUSY
// }
func (e *Error) Is(err error) bool {
switch c := err.(type) {
case ErrorCode:
return c == e.Code()
case ExtendedErrorCode:
return c == e.ExtendedCode()
}
return false
}
// Temporary returns true for [BUSY] errors.
func (e *Error) Temporary() bool {
return e.Code() == BUSY
}
// Timeout returns true for [BUSY_TIMEOUT] errors.
func (e *Error) Timeout() bool {
return e.ExtendedCode() == BUSY_TIMEOUT
}
// SQL returns the SQL starting at the token that triggered a syntax error.
func (e *Error) SQL() string {
return e.sql
}
// Error implements the error interface.
func (e ErrorCode) Error() string {
switch e {
case _OK:
return "sqlite3: not an error"
case _ROW:
return "sqlite3: another row available"
case _DONE:
return "sqlite3: no more rows available"
case ERROR:
return "sqlite3: SQL logic error"
case INTERNAL:
break
case PERM:
return "sqlite3: access permission denied"
case ABORT:
return "sqlite3: query aborted"
case BUSY:
return "sqlite3: database is locked"
case LOCKED:
return "sqlite3: database table is locked"
case NOMEM:
return "sqlite3: out of memory"
case READONLY:
return "sqlite3: attempt to write a readonly database"
case INTERRUPT:
return "sqlite3: interrupted"
case IOERR:
return "sqlite3: disk I/O error"
case CORRUPT:
return "sqlite3: database disk image is malformed"
case NOTFOUND:
return "sqlite3: unknown operation"
case FULL:
return "sqlite3: database or disk is full"
case CANTOPEN:
return "sqlite3: unable to open database file"
case PROTOCOL:
return "sqlite3: locking protocol"
case FORMAT:
break
case SCHEMA:
return "sqlite3: database schema has changed"
case TOOBIG:
return "sqlite3: string or blob too big"
case CONSTRAINT:
return "sqlite3: constraint failed"
case MISMATCH:
return "sqlite3: datatype mismatch"
case MISUSE:
return "sqlite3: bad parameter or other API misuse"
case NOLFS:
break
case AUTH:
return "sqlite3: authorization denied"
case EMPTY:
break
case RANGE:
return "sqlite3: column index out of range"
case NOTADB:
return "sqlite3: file is not a database"
case NOTICE:
return "sqlite3: notification message"
case WARNING:
return "sqlite3: warning message"
}
return "sqlite3: unknown error"
}
// Temporary returns true for [BUSY] errors.
func (e ErrorCode) Temporary() bool {
return e == BUSY
}
// Error implements the error interface.
func (e ExtendedErrorCode) Error() string {
switch x := ErrorCode(e); {
case e == ABORT_ROLLBACK:
return "sqlite3: abort due to ROLLBACK"
case x < _ROW:
return x.Error()
case e == _ROW:
return "sqlite3: another row available"
case e == _DONE:
return "sqlite3: no more rows available"
}
return "sqlite3: unknown error"
}
// Is tests whether this error matches a given [ErrorCode].
func (e ExtendedErrorCode) Is(err error) bool {
c, ok := err.(ErrorCode)
return ok && c == ErrorCode(e)
}
// Temporary returns true for [BUSY] errors.
func (e ExtendedErrorCode) Temporary() bool {
return ErrorCode(e) == BUSY
}
// Timeout returns true for [BUSY_TIMEOUT] errors.
func (e ExtendedErrorCode) Timeout() bool {
return e == BUSY_TIMEOUT
}
type errorString string
func (e errorString) Error() string { return string(e) }
const (
nilErr = errorString("sqlite3: invalid memory address or null pointer dereference")
oomErr = errorString("sqlite3: out of memory")
rangeErr = errorString("sqlite3: index out of range")
noNulErr = errorString("sqlite3: missing NUL terminator")
noGlobalErr = errorString("sqlite3: could not find global: ")
noFuncErr = errorString("sqlite3: could not find function: ")
binaryErr = errorString("sqlite3: no SQLite binary embed/set/loaded")
timeErr = errorString("sqlite3: invalid time value")
whenceErr = errorString("sqlite3: invalid whence")
offsetErr = errorString("sqlite3: invalid offset")
)
func assertErr() errorString {
msg := "sqlite3: assertion failed"
if _, file, line, ok := runtime.Caller(1); ok {
msg += " (" + file + ":" + strconv.Itoa(line) + ")"
}
return errorString(msg)
}
func finalizer[T any](skip int) func(*T) {
msg := fmt.Sprintf("sqlite3: %T not closed", new(T))
if _, file, line, ok := runtime.Caller(skip + 1); ok && skip >= 0 {
msg += " (" + file + ":" + strconv.Itoa(line) + ")"
}
return func(*T) { panic(errorString(msg)) }
}
+135
View File
@@ -0,0 +1,135 @@
package sqlite3
import (
"bytes"
"math"
"github.com/tetratelabs/wazero/api"
)
type memory struct {
mod api.Module
}
func (m memory) view(ptr uint32, size uint64) []byte {
if ptr == 0 {
panic(nilErr)
}
if size > math.MaxUint32 {
panic(rangeErr)
}
buf, ok := m.mod.Memory().Read(ptr, uint32(size))
if !ok {
panic(rangeErr)
}
return buf
}
func (m memory) readUint8(ptr uint32) uint8 {
if ptr == 0 {
panic(nilErr)
}
v, ok := m.mod.Memory().ReadByte(ptr)
if !ok {
panic(rangeErr)
}
return v
}
func (m memory) writeUint8(ptr uint32, v uint8) {
if ptr == 0 {
panic(nilErr)
}
ok := m.mod.Memory().WriteByte(ptr, v)
if !ok {
panic(rangeErr)
}
}
func (m memory) readUint32(ptr uint32) uint32 {
if ptr == 0 {
panic(nilErr)
}
v, ok := m.mod.Memory().ReadUint32Le(ptr)
if !ok {
panic(rangeErr)
}
return v
}
func (m memory) writeUint32(ptr uint32, v uint32) {
if ptr == 0 {
panic(nilErr)
}
ok := m.mod.Memory().WriteUint32Le(ptr, v)
if !ok {
panic(rangeErr)
}
}
func (m memory) readUint64(ptr uint32) uint64 {
if ptr == 0 {
panic(nilErr)
}
v, ok := m.mod.Memory().ReadUint64Le(ptr)
if !ok {
panic(rangeErr)
}
return v
}
func (m memory) writeUint64(ptr uint32, v uint64) {
if ptr == 0 {
panic(nilErr)
}
ok := m.mod.Memory().WriteUint64Le(ptr, v)
if !ok {
panic(rangeErr)
}
}
func (m memory) readFloat64(ptr uint32) float64 {
return math.Float64frombits(m.readUint64(ptr))
}
func (m memory) writeFloat64(ptr uint32, v float64) {
m.writeUint64(ptr, math.Float64bits(v))
}
func (m memory) readString(ptr, maxlen uint32) string {
if ptr == 0 {
panic(nilErr)
}
switch maxlen {
case 0:
return ""
case math.MaxUint32:
// avoid overflow
default:
maxlen = maxlen + 1
}
mem := m.mod.Memory()
buf, ok := mem.Read(ptr, maxlen)
if !ok {
buf, ok = mem.Read(ptr, mem.Size()-ptr)
if !ok {
panic(rangeErr)
}
}
if i := bytes.IndexByte(buf, 0); i < 0 {
panic(noNulErr)
} else {
return string(buf[:i])
}
}
func (m memory) writeBytes(ptr uint32, b []byte) {
buf := m.view(ptr, uint64(len(b)))
copy(buf, b)
}
func (m memory) writeString(ptr uint32, s string) {
buf := m.view(ptr, uint64(len(s)+1))
buf[len(s)] = 0
copy(buf, s)
}
+342
View File
@@ -0,0 +1,342 @@
// Package sqlite3 wraps the C SQLite API.
package sqlite3
import (
"context"
"io"
"math"
"os"
"sync"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
)
// Configure SQLite WASM.
//
// Importing package embed initializes these
// with an appropriate build of SQLite:
//
// import _ "github.com/ncruces/go-sqlite3/embed"
var (
Binary []byte // WASM binary to load.
Path string // Path to load the binary from.
)
var sqlite3 struct {
once sync.Once
runtime wazero.Runtime
compiled wazero.CompiledModule
err error
}
func instantiateModule() (*module, error) {
ctx := context.Background()
sqlite3.once.Do(compileModule)
if sqlite3.err != nil {
return nil, sqlite3.err
}
cfg := wazero.NewModuleConfig().WithStartFunctions("_initialize")
mod, err := sqlite3.runtime.InstantiateModule(ctx, sqlite3.compiled, cfg)
if err != nil {
return nil, err
}
return newModule(mod)
}
func compileModule() {
ctx := context.Background()
sqlite3.runtime = wazero.NewRuntime(ctx)
vfsInstantiate(ctx, sqlite3.runtime)
bin := Binary
if bin == nil && Path != "" {
bin, sqlite3.err = os.ReadFile(Path)
if sqlite3.err != nil {
return
}
}
if bin == nil {
sqlite3.err = binaryErr
return
}
sqlite3.compiled, sqlite3.err = sqlite3.runtime.CompileModule(ctx, bin)
}
type module struct {
ctx context.Context
mem memory
api sqliteAPI
vfs io.Closer
}
func newModule(mod api.Module) (m *module, err error) {
m = &module{}
m.mem = memory{mod}
m.ctx, m.vfs = vfsContext(context.Background())
getFun := func(name string) api.Function {
f := mod.ExportedFunction(name)
if f == nil {
err = noFuncErr + errorString(name)
return nil
}
return f
}
getVal := func(name string) uint32 {
global := mod.ExportedGlobal(name)
if global == nil {
err = noGlobalErr + errorString(name)
return 0
}
return m.mem.readUint32(uint32(global.Get()))
}
m.api = sqliteAPI{
free: getFun("free"),
malloc: getFun("malloc"),
destructor: uint64(getVal("malloc_destructor")),
errcode: getFun("sqlite3_errcode"),
errstr: getFun("sqlite3_errstr"),
errmsg: getFun("sqlite3_errmsg"),
erroff: getFun("sqlite3_error_offset"),
open: getFun("sqlite3_open_v2"),
close: getFun("sqlite3_close"),
closeZombie: getFun("sqlite3_close_v2"),
prepare: getFun("sqlite3_prepare_v3"),
finalize: getFun("sqlite3_finalize"),
reset: getFun("sqlite3_reset"),
step: getFun("sqlite3_step"),
exec: getFun("sqlite3_exec"),
clearBindings: getFun("sqlite3_clear_bindings"),
bindCount: getFun("sqlite3_bind_parameter_count"),
bindIndex: getFun("sqlite3_bind_parameter_index"),
bindName: getFun("sqlite3_bind_parameter_name"),
bindNull: getFun("sqlite3_bind_null"),
bindInteger: getFun("sqlite3_bind_int64"),
bindFloat: getFun("sqlite3_bind_double"),
bindText: getFun("sqlite3_bind_text64"),
bindBlob: getFun("sqlite3_bind_blob64"),
bindZeroBlob: getFun("sqlite3_bind_zeroblob64"),
columnCount: getFun("sqlite3_column_count"),
columnName: getFun("sqlite3_column_name"),
columnType: getFun("sqlite3_column_type"),
columnInteger: getFun("sqlite3_column_int64"),
columnFloat: getFun("sqlite3_column_double"),
columnText: getFun("sqlite3_column_text"),
columnBlob: getFun("sqlite3_column_blob"),
columnBytes: getFun("sqlite3_column_bytes"),
autocommit: getFun("sqlite3_get_autocommit"),
lastRowid: getFun("sqlite3_last_insert_rowid"),
changes: getFun("sqlite3_changes64"),
blobOpen: getFun("sqlite3_blob_open"),
blobClose: getFun("sqlite3_blob_close"),
blobReopen: getFun("sqlite3_blob_reopen"),
blobBytes: getFun("sqlite3_blob_bytes"),
blobRead: getFun("sqlite3_blob_read"),
blobWrite: getFun("sqlite3_blob_write"),
backupInit: getFun("sqlite3_backup_init"),
backupStep: getFun("sqlite3_backup_step"),
backupFinish: getFun("sqlite3_backup_finish"),
backupRemaining: getFun("sqlite3_backup_remaining"),
backupPageCount: getFun("sqlite3_backup_pagecount"),
interrupt: getVal("sqlite3_interrupt_offset"),
}
if err != nil {
return nil, err
}
return m, nil
}
func (m *module) close() error {
err := m.mem.mod.Close(m.ctx)
m.vfs.Close()
return err
}
func (m *module) error(rc uint64, handle uint32, sql ...string) error {
if rc == _OK {
return nil
}
err := Error{code: rc}
if err.Code() == NOMEM || err.ExtendedCode() == IOERR_NOMEM {
panic(oomErr)
}
var r []uint64
r = m.call(m.api.errstr, rc)
if r != nil {
err.str = m.mem.readString(uint32(r[0]), _MAX_STRING)
}
r = m.call(m.api.errmsg, uint64(handle))
if r != nil {
err.msg = m.mem.readString(uint32(r[0]), _MAX_STRING)
}
if sql != nil {
r = m.call(m.api.erroff, uint64(handle))
if r != nil && r[0] != math.MaxUint32 {
err.sql = sql[0][r[0]:]
}
}
switch err.msg {
case err.str, "not an error":
err.msg = ""
}
return &err
}
func (m *module) call(fn api.Function, params ...uint64) []uint64 {
r, err := fn.Call(m.ctx, params...)
if err != nil {
// The module closed or panicked; release resources.
m.vfs.Close()
panic(err)
}
return r
}
func (m *module) free(ptr uint32) {
if ptr == 0 {
return
}
m.call(m.api.free, uint64(ptr))
}
func (m *module) new(size uint64) uint32 {
if size > _MAX_ALLOCATION_SIZE {
panic(oomErr)
}
r := m.call(m.api.malloc, size)
ptr := uint32(r[0])
if ptr == 0 && size != 0 {
panic(oomErr)
}
return ptr
}
func (m *module) newBytes(b []byte) uint32 {
if b == nil {
return 0
}
ptr := m.new(uint64(len(b)))
m.mem.writeBytes(ptr, b)
return ptr
}
func (m *module) newString(s string) uint32 {
ptr := m.new(uint64(len(s) + 1))
m.mem.writeString(ptr, s)
return ptr
}
func (m *module) newArena(size uint64) arena {
return arena{
m: m,
base: m.new(size),
size: uint32(size),
}
}
type arena struct {
m *module
base uint32
next uint32
size uint32
ptrs []uint32
}
func (a *arena) free() {
if a.m == nil {
return
}
a.reset()
a.m.free(a.base)
a.m = nil
}
func (a *arena) reset() {
for _, ptr := range a.ptrs {
a.m.free(ptr)
}
a.ptrs = nil
a.next = 0
}
func (a *arena) new(size uint64) uint32 {
if size <= uint64(a.size-a.next) {
ptr := a.base + a.next
a.next += uint32(size)
return ptr
}
ptr := a.m.new(size)
a.ptrs = append(a.ptrs, ptr)
return ptr
}
func (a *arena) string(s string) uint32 {
ptr := a.new(uint64(len(s) + 1))
a.m.mem.writeString(ptr, s)
return ptr
}
type sqliteAPI struct {
free api.Function
malloc api.Function
destructor uint64
errcode api.Function
errstr api.Function
errmsg api.Function
erroff api.Function
open api.Function
close api.Function
closeZombie api.Function
prepare api.Function
finalize api.Function
reset api.Function
step api.Function
exec api.Function
clearBindings api.Function
bindNull api.Function
bindCount api.Function
bindIndex api.Function
bindName api.Function
bindInteger api.Function
bindFloat api.Function
bindText api.Function
bindBlob api.Function
bindZeroBlob api.Function
columnCount api.Function
columnName api.Function
columnType api.Function
columnInteger api.Function
columnFloat api.Function
columnText api.Function
columnBlob api.Function
columnBytes api.Function
autocommit api.Function
lastRowid api.Function
changes api.Function
blobOpen api.Function
blobClose api.Function
blobReopen api.Function
blobBytes api.Function
blobRead api.Function
blobWrite api.Function
backupInit api.Function
backupStep api.Function
backupFinish api.Function
backupRemaining api.Function
backupPageCount api.Function
interrupt uint32
}
+408
View File
@@ -0,0 +1,408 @@
package sqlite3
import (
"math"
"time"
)
// Stmt is a prepared statement object.
//
// https://www.sqlite.org/c3ref/stmt.html
type Stmt struct {
c *Conn
handle uint32
err error
}
// Close destroys the prepared statement object.
//
// It is safe to close a nil, zero or closed Stmt.
//
// https://www.sqlite.org/c3ref/finalize.html
func (s *Stmt) Close() error {
if s == nil || s.handle == 0 {
return nil
}
r := s.c.call(s.c.api.finalize, uint64(s.handle))
s.handle = 0
return s.c.error(r[0])
}
// Reset resets the prepared statement object.
//
// https://www.sqlite.org/c3ref/reset.html
func (s *Stmt) Reset() error {
r := s.c.call(s.c.api.reset, uint64(s.handle))
s.err = nil
return s.c.error(r[0])
}
// ClearBindings resets all bindings on the prepared statement.
//
// https://www.sqlite.org/c3ref/clear_bindings.html
func (s *Stmt) ClearBindings() error {
r := s.c.call(s.c.api.clearBindings, uint64(s.handle))
return s.c.error(r[0])
}
// Step evaluates the SQL statement.
// If the SQL statement being executed returns any data,
// then true is returned each time a new row of data is ready for processing by the caller.
// The values may be accessed using the Column access functions.
// Step is called again to retrieve the next row of data.
// If an error has occurred, Step returns false;
// call [Stmt.Err] or [Stmt.Reset] to get the error.
//
// https://www.sqlite.org/c3ref/step.html
func (s *Stmt) Step() bool {
s.c.checkInterrupt()
r := s.c.call(s.c.api.step, uint64(s.handle))
if r[0] == _ROW {
return true
}
if r[0] == _DONE {
s.err = nil
} else {
s.err = s.c.error(r[0])
}
return false
}
// Err gets the last error occurred during [Stmt.Step].
// Err returns nil after [Stmt.Reset] is called.
//
// https://www.sqlite.org/c3ref/step.html
func (s *Stmt) Err() error {
return s.err
}
// Exec is a convenience function that repeatedly calls [Stmt.Step] until it returns false,
// then calls [Stmt.Reset] to reset the statement and get any error that occurred.
func (s *Stmt) Exec() error {
for s.Step() {
}
return s.Reset()
}
// BindCount returns the number of SQL parameters in the prepared statement.
//
// https://www.sqlite.org/c3ref/bind_parameter_count.html
func (s *Stmt) BindCount() int {
r := s.c.call(s.c.api.bindCount,
uint64(s.handle))
return int(r[0])
}
// BindIndex returns the index of a parameter in the prepared statement
// given its name.
//
// https://www.sqlite.org/c3ref/bind_parameter_index.html
func (s *Stmt) BindIndex(name string) int {
defer s.c.arena.reset()
namePtr := s.c.arena.string(name)
r := s.c.call(s.c.api.bindIndex,
uint64(s.handle), uint64(namePtr))
return int(r[0])
}
// BindName returns the name of a parameter in the prepared statement.
// The leftmost SQL parameter has an index of 1.
//
// https://www.sqlite.org/c3ref/bind_parameter_name.html
func (s *Stmt) BindName(param int) string {
r := s.c.call(s.c.api.bindName,
uint64(s.handle), uint64(param))
ptr := uint32(r[0])
if ptr == 0 {
return ""
}
return s.c.mem.readString(ptr, _MAX_STRING)
}
// BindBool binds a bool to the prepared statement.
// The leftmost SQL parameter has an index of 1.
// SQLite does not have a separate boolean storage class.
// Instead, boolean values are stored as integers 0 (false) and 1 (true).
//
// https://www.sqlite.org/c3ref/bind_blob.html
func (s *Stmt) BindBool(param int, value bool) error {
if value {
return s.BindInt64(param, 1)
}
return s.BindInt64(param, 0)
}
// BindInt binds an int to the prepared statement.
// The leftmost SQL parameter has an index of 1.
//
// https://www.sqlite.org/c3ref/bind_blob.html
func (s *Stmt) BindInt(param int, value int) error {
return s.BindInt64(param, int64(value))
}
// BindInt64 binds an int64 to the prepared statement.
// The leftmost SQL parameter has an index of 1.
//
// https://www.sqlite.org/c3ref/bind_blob.html
func (s *Stmt) BindInt64(param int, value int64) error {
r := s.c.call(s.c.api.bindInteger,
uint64(s.handle), uint64(param), uint64(value))
return s.c.error(r[0])
}
// BindFloat binds a float64 to the prepared statement.
// The leftmost SQL parameter has an index of 1.
//
// https://www.sqlite.org/c3ref/bind_blob.html
func (s *Stmt) BindFloat(param int, value float64) error {
r := s.c.call(s.c.api.bindFloat,
uint64(s.handle), uint64(param), math.Float64bits(value))
return s.c.error(r[0])
}
// BindText binds a string to the prepared statement.
// The leftmost SQL parameter has an index of 1.
//
// https://www.sqlite.org/c3ref/bind_blob.html
func (s *Stmt) BindText(param int, value string) error {
ptr := s.c.newString(value)
r := s.c.call(s.c.api.bindText,
uint64(s.handle), uint64(param),
uint64(ptr), uint64(len(value)),
s.c.api.destructor, _UTF8)
return s.c.error(r[0])
}
// BindBlob binds a []byte to the prepared statement.
// The leftmost SQL parameter has an index of 1.
// Binding a nil slice is the same as calling [Stmt.BindNull].
//
// https://www.sqlite.org/c3ref/bind_blob.html
func (s *Stmt) BindBlob(param int, value []byte) error {
ptr := s.c.newBytes(value)
r := s.c.call(s.c.api.bindBlob,
uint64(s.handle), uint64(param),
uint64(ptr), uint64(len(value)),
s.c.api.destructor)
return s.c.error(r[0])
}
// BindZeroBlob binds a zero-filled, length n BLOB to the prepared statement.
// The leftmost SQL parameter has an index of 1.
//
// https://www.sqlite.org/c3ref/bind_blob.html
func (s *Stmt) BindZeroBlob(param int, n int64) error {
r := s.c.call(s.c.api.bindZeroBlob,
uint64(s.handle), uint64(param), uint64(n))
return s.c.error(r[0])
}
// BindNull binds a NULL to the prepared statement.
// The leftmost SQL parameter has an index of 1.
//
// https://www.sqlite.org/c3ref/bind_blob.html
func (s *Stmt) BindNull(param int) error {
r := s.c.call(s.c.api.bindNull,
uint64(s.handle), uint64(param))
return s.c.error(r[0])
}
// BindTime binds a [time.Time] to the prepared statement.
// The leftmost SQL parameter has an index of 1.
//
// https://www.sqlite.org/c3ref/bind_blob.html
func (s *Stmt) BindTime(param int, value time.Time, format TimeFormat) error {
switch v := format.Encode(value).(type) {
case string:
s.BindText(param, v)
case int64:
s.BindInt64(param, v)
case float64:
s.BindFloat(param, v)
default:
panic(assertErr())
}
return nil
}
// ColumnCount returns the number of columns in a result set.
//
// https://www.sqlite.org/c3ref/column_count.html
func (s *Stmt) ColumnCount() int {
r := s.c.call(s.c.api.columnCount,
uint64(s.handle))
return int(r[0])
}
// ColumnName returns the name of the result column.
// The leftmost column of the result set has the index 0.
//
// https://www.sqlite.org/c3ref/column_name.html
func (s *Stmt) ColumnName(col int) string {
r := s.c.call(s.c.api.columnName,
uint64(s.handle), uint64(col))
ptr := uint32(r[0])
if ptr == 0 {
panic(oomErr)
}
return s.c.mem.readString(ptr, _MAX_STRING)
}
// ColumnType returns the initial [Datatype] of the result column.
// The leftmost column of the result set has the index 0.
//
// https://www.sqlite.org/c3ref/column_blob.html
func (s *Stmt) ColumnType(col int) Datatype {
r := s.c.call(s.c.api.columnType,
uint64(s.handle), uint64(col))
return Datatype(r[0])
}
// ColumnBool returns the value of the result column as a bool.
// The leftmost column of the result set has the index 0.
// SQLite does not have a separate boolean storage class.
// Instead, boolean values are retrieved as integers,
// with 0 converted to false and any other value to true.
//
// https://www.sqlite.org/c3ref/column_blob.html
func (s *Stmt) ColumnBool(col int) bool {
if i := s.ColumnInt64(col); i != 0 {
return true
}
return false
}
// ColumnInt returns the value of the result column as an int.
// The leftmost column of the result set has the index 0.
//
// https://www.sqlite.org/c3ref/column_blob.html
func (s *Stmt) ColumnInt(col int) int {
return int(s.ColumnInt64(col))
}
// ColumnInt64 returns the value of the result column as an int64.
// The leftmost column of the result set has the index 0.
//
// https://www.sqlite.org/c3ref/column_blob.html
func (s *Stmt) ColumnInt64(col int) int64 {
r := s.c.call(s.c.api.columnInteger,
uint64(s.handle), uint64(col))
return int64(r[0])
}
// ColumnFloat returns the value of the result column as a float64.
// The leftmost column of the result set has the index 0.
//
// https://www.sqlite.org/c3ref/column_blob.html
func (s *Stmt) ColumnFloat(col int) float64 {
r := s.c.call(s.c.api.columnFloat,
uint64(s.handle), uint64(col))
return math.Float64frombits(r[0])
}
// ColumnTime returns the value of the result column as a [time.Time].
// The leftmost column of the result set has the index 0.
//
// https://www.sqlite.org/c3ref/column_blob.html
func (s *Stmt) ColumnTime(col int, format TimeFormat) time.Time {
var v any
switch s.ColumnType(col) {
case INTEGER:
v = s.ColumnInt64(col)
case FLOAT:
v = s.ColumnFloat(col)
case TEXT, BLOB:
v = s.ColumnText(col)
case NULL:
return time.Time{}
default:
panic(assertErr())
}
t, err := format.Decode(v)
if err != nil {
s.err = err
}
return t
}
// ColumnText returns the value of the result column as a string.
// The leftmost column of the result set has the index 0.
//
// https://www.sqlite.org/c3ref/column_blob.html
func (s *Stmt) ColumnText(col int) string {
return string(s.ColumnRawText(col))
}
// ColumnBlob appends to buf and returns
// the value of the result column as a []byte.
// The leftmost column of the result set has the index 0.
//
// https://www.sqlite.org/c3ref/column_blob.html
func (s *Stmt) ColumnBlob(col int, buf []byte) []byte {
return append(buf, s.ColumnRawBlob(col)...)
}
// ColumnRawText returns the value of the result column as a []byte.
// The []byte is owned by SQLite and may be invalidated by
// subsequent calls to [Stmt] methods.
// The leftmost column of the result set has the index 0.
//
// https://www.sqlite.org/c3ref/column_blob.html
func (s *Stmt) ColumnRawText(col int) []byte {
r := s.c.call(s.c.api.columnText,
uint64(s.handle), uint64(col))
ptr := uint32(r[0])
if ptr == 0 {
r = s.c.call(s.c.api.errcode, uint64(s.c.handle))
s.err = s.c.error(r[0])
return nil
}
r = s.c.call(s.c.api.columnBytes,
uint64(s.handle), uint64(col))
return s.c.mem.view(ptr, r[0])
}
// ColumnRawBlob returns the value of the result column as a []byte.
// The []byte is owned by SQLite and may be invalidated by
// subsequent calls to [Stmt] methods.
// The leftmost column of the result set has the index 0.
//
// https://www.sqlite.org/c3ref/column_blob.html
func (s *Stmt) ColumnRawBlob(col int) []byte {
r := s.c.call(s.c.api.columnBlob,
uint64(s.handle), uint64(col))
ptr := uint32(r[0])
if ptr == 0 {
r = s.c.call(s.c.api.errcode, uint64(s.c.handle))
s.err = s.c.error(r[0])
return nil
}
r = s.c.call(s.c.api.columnBytes,
uint64(s.handle), uint64(col))
return s.c.mem.view(ptr, r[0])
}
// Return true if stmt is an empty SQL statement.
// This is used as an optimization.
// It's OK to always return false here.
func emptyStatement(stmt string) bool {
for _, b := range []byte(stmt) {
switch b {
case ' ', '\n', '\r', '\t', '\v', '\f':
case ';':
default:
return false
}
}
return true
}
+339
View File
@@ -0,0 +1,339 @@
package sqlite3
import (
"math"
"strconv"
"strings"
"time"
"github.com/ncruces/julianday"
)
// TimeFormat specifies how to encode/decode time values.
//
// See the documentation for the [TimeFormatDefault] constant
// for formats recognized by SQLite.
//
// https://www.sqlite.org/lang_datefunc.html
type TimeFormat string
// TimeFormats recognized by SQLite to encode/decode time values.
//
// https://www.sqlite.org/lang_datefunc.html
const (
TimeFormatDefault TimeFormat = "" // time.RFC3339Nano
// Text formats
TimeFormat1 TimeFormat = "2006-01-02"
TimeFormat2 TimeFormat = "2006-01-02 15:04"
TimeFormat3 TimeFormat = "2006-01-02 15:04:05"
TimeFormat4 TimeFormat = "2006-01-02 15:04:05.000"
TimeFormat5 TimeFormat = "2006-01-02T15:04"
TimeFormat6 TimeFormat = "2006-01-02T15:04:05"
TimeFormat7 TimeFormat = "2006-01-02T15:04:05.000"
TimeFormat8 TimeFormat = "15:04"
TimeFormat9 TimeFormat = "15:04:05"
TimeFormat10 TimeFormat = "15:04:05.000"
TimeFormat2TZ = TimeFormat2 + "Z07:00"
TimeFormat3TZ = TimeFormat3 + "Z07:00"
TimeFormat4TZ = TimeFormat4 + "Z07:00"
TimeFormat5TZ = TimeFormat5 + "Z07:00"
TimeFormat6TZ = TimeFormat6 + "Z07:00"
TimeFormat7TZ = TimeFormat7 + "Z07:00"
TimeFormat8TZ = TimeFormat8 + "Z07:00"
TimeFormat9TZ = TimeFormat9 + "Z07:00"
TimeFormat10TZ = TimeFormat10 + "Z07:00"
// Numeric formats
TimeFormatJulianDay TimeFormat = "julianday"
TimeFormatUnix TimeFormat = "unixepoch"
TimeFormatUnixFrac TimeFormat = "unixepoch_frac"
TimeFormatUnixMilli TimeFormat = "unixepoch_milli" // not an SQLite format
TimeFormatUnixMicro TimeFormat = "unixepoch_micro" // not an SQLite format
TimeFormatUnixNano TimeFormat = "unixepoch_nano" // not an SQLite format
// Auto
TimeFormatAuto TimeFormat = "auto"
)
// Encode encodes a time value using this format.
//
// [TimeFormatDefault] and [TimeFormatAuto] encode using [time.RFC3339Nano],
// with nanosecond accuracy, and preserving any timezone offset.
//
// This is the format used by the [database/sql] driver:
// [database/sql.Row.Scan] will decode as [time.Time]
// values encoded with [time.RFC3339Nano].
//
// Time values encoded with [time.RFC3339Nano] cannot be sorted as strings
// to produce a time-ordered sequence.
//
// Assuming that the time zones of the time values are the same (e.g., all in UTC),
// and expressed using the same string (e.g., all "Z" or all "+00:00"),
// use the TIME [collating sequence] to produce a time-ordered sequence.
//
// Otherwise, use [TimeFormat7] for time-ordered encoding.
//
// Formats [TimeFormat1] through [TimeFormat10]
// convert time values to UTC before encoding.
//
// Returns a string for the text formats,
// a float64 for [TimeFormatJulianDay] and [TimeFormatUnixFrac],
// or an int64 for the other numeric formats.
//
// https://www.sqlite.org/lang_datefunc.html
//
// [collating sequence]: https://www.sqlite.org/datatype3.html#collating_sequences
func (f TimeFormat) Encode(t time.Time) any {
switch f {
// Numeric formats
case TimeFormatJulianDay:
return julianday.Float(t)
case TimeFormatUnix:
return t.Unix()
case TimeFormatUnixFrac:
return float64(t.Unix()) + float64(t.Nanosecond())*1e-9
case TimeFormatUnixMilli:
return t.UnixMilli()
case TimeFormatUnixMicro:
return t.UnixMicro()
case TimeFormatUnixNano:
return t.UnixNano()
// Special formats
case TimeFormatDefault, TimeFormatAuto:
f = time.RFC3339Nano
// SQLite assumes UTC if unspecified.
case
TimeFormat1, TimeFormat2,
TimeFormat3, TimeFormat4,
TimeFormat5, TimeFormat6,
TimeFormat7, TimeFormat8,
TimeFormat9, TimeFormat10:
t = t.UTC()
}
return t.Format(string(f))
}
// Decode decodes a time value using this format.
//
// The time value can be a string, an int64, or a float64.
//
// Formats [TimeFormat8] through [TimeFormat10]
// (and [TimeFormat8TZ] through [TimeFormat10TZ])
// assume a date of 2000-01-01.
//
// The timezone indicator and fractional seconds are always optional
// for formats [TimeFormat2] through [TimeFormat10]
// (and [TimeFormat2TZ] through [TimeFormat10TZ]).
//
// [TimeFormatAuto] implements (and extends) the SQLite auto modifier.
// Julian day numbers are safe to use for historical dates,
// from 4712BC through 9999AD.
// Unix timestamps (expressed in seconds, milliseconds, microseconds, or nanoseconds)
// are safe to use for current events, from at least 1980 through at least 2260.
// Unix timestamps before 1980 and after 9999 may be misinterpreted as julian day numbers,
// or have the wrong time unit.
//
// https://www.sqlite.org/lang_datefunc.html
func (f TimeFormat) Decode(v any) (time.Time, error) {
switch f {
// Numeric formats
case TimeFormatJulianDay:
switch v := v.(type) {
case string:
return julianday.Parse(v)
case float64:
return julianday.FloatTime(v), nil
case int64:
return julianday.Time(v, 0), nil
default:
return time.Time{}, timeErr
}
case TimeFormatUnix, TimeFormatUnixFrac:
if s, ok := v.(string); ok {
f, err := strconv.ParseFloat(s, 64)
if err != nil {
return time.Time{}, err
}
v = f
}
switch v := v.(type) {
case float64:
sec, frac := math.Modf(v)
nsec := math.Floor(frac * 1e9)
return time.Unix(int64(sec), int64(nsec)), nil
case int64:
return time.Unix(v, 0), nil
default:
return time.Time{}, timeErr
}
case TimeFormatUnixMilli:
if s, ok := v.(string); ok {
i, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return time.Time{}, err
}
v = i
}
switch v := v.(type) {
case float64:
return time.UnixMilli(int64(math.Floor(v))), nil
case int64:
return time.UnixMilli(int64(v)), nil
default:
return time.Time{}, timeErr
}
case TimeFormatUnixMicro:
if s, ok := v.(string); ok {
i, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return time.Time{}, err
}
v = i
}
switch v := v.(type) {
case float64:
return time.UnixMicro(int64(math.Floor(v))), nil
case int64:
return time.UnixMicro(int64(v)), nil
default:
return time.Time{}, timeErr
}
case TimeFormatUnixNano:
if s, ok := v.(string); ok {
i, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return time.Time{}, timeErr
}
v = i
}
switch v := v.(type) {
case float64:
return time.Unix(0, int64(math.Floor(v))), nil
case int64:
return time.Unix(0, int64(v)), nil
default:
return time.Time{}, timeErr
}
// Special formats
case TimeFormatAuto:
switch s := v.(type) {
case string:
i, err := strconv.ParseInt(s, 10, 64)
if err == nil {
v = i
break
}
f, err := strconv.ParseFloat(s, 64)
if err == nil {
v = f
break
}
dates := []TimeFormat{
TimeFormat6TZ, TimeFormat6, TimeFormat3TZ, TimeFormat3,
TimeFormat5TZ, TimeFormat5, TimeFormat2TZ, TimeFormat2,
TimeFormat1,
}
for _, f := range dates {
t, err := time.Parse(string(f), s)
if err == nil {
return t, nil
}
}
times := []TimeFormat{
TimeFormat9TZ, TimeFormat9, TimeFormat8TZ, TimeFormat8,
}
for _, f := range times {
t, err := time.Parse(string(f), s)
if err == nil {
return t.AddDate(2000, 0, 0), nil
}
}
}
switch v := v.(type) {
case float64:
if 0 <= v && v < 5373484.5 {
return TimeFormatJulianDay.Decode(v)
}
if v < 253402300800 {
return TimeFormatUnixFrac.Decode(v)
}
if v < 253402300800_000 {
return TimeFormatUnixMilli.Decode(v)
}
if v < 253402300800_000000 {
return TimeFormatUnixMicro.Decode(v)
}
return TimeFormatUnixNano.Decode(v)
case int64:
if 0 <= v && v < 5373485 {
return TimeFormatJulianDay.Decode(v)
}
if v < 253402300800 {
return TimeFormatUnixFrac.Decode(v)
}
if v < 253402300800_000 {
return TimeFormatUnixMilli.Decode(v)
}
if v < 253402300800_000000 {
return TimeFormatUnixMicro.Decode(v)
}
return TimeFormatUnixNano.Decode(v)
default:
return time.Time{}, timeErr
}
case
TimeFormat2, TimeFormat2TZ,
TimeFormat3, TimeFormat3TZ,
TimeFormat4, TimeFormat4TZ,
TimeFormat5, TimeFormat5TZ,
TimeFormat6, TimeFormat6TZ,
TimeFormat7, TimeFormat7TZ:
s, ok := v.(string)
if !ok {
return time.Time{}, timeErr
}
return f.parseRelaxed(s)
case
TimeFormat8, TimeFormat8TZ,
TimeFormat9, TimeFormat9TZ,
TimeFormat10, TimeFormat10TZ:
s, ok := v.(string)
if !ok {
return time.Time{}, timeErr
}
t, err := f.parseRelaxed(s)
return t.AddDate(2000, 0, 0), err
default:
s, ok := v.(string)
if !ok {
return time.Time{}, timeErr
}
if f == "" {
f = time.RFC3339Nano
}
return time.Parse(string(f), s)
}
}
func (f TimeFormat) parseRelaxed(s string) (time.Time, error) {
fs := string(f)
fs = strings.TrimSuffix(fs, "Z07:00")
fs = strings.TrimSuffix(fs, ".000")
t, err := time.Parse(fs+"Z07:00", s)
if err != nil {
return time.Parse(fs, s)
}
return t, nil
}
+202
View File
@@ -0,0 +1,202 @@
package sqlite3
import (
"context"
"errors"
"fmt"
"math/rand"
"runtime"
"strconv"
)
// Tx is an in-progress database transaction.
//
// https://www.sqlite.org/lang_transaction.html
type Tx struct {
c *Conn
}
// Begin starts a deferred transaction.
//
// https://www.sqlite.org/lang_transaction.html
func (c *Conn) Begin() Tx {
// BEGIN even if interrupted.
err := c.txExecInterrupted(`BEGIN DEFERRED`)
if err != nil {
panic(err)
}
return Tx{c}
}
// BeginImmediate starts an immediate transaction.
//
// https://www.sqlite.org/lang_transaction.html
func (c *Conn) BeginImmediate() (Tx, error) {
err := c.Exec(`BEGIN IMMEDIATE`)
if err != nil {
return Tx{}, err
}
return Tx{c}, nil
}
// BeginExclusive starts an exclusive transaction.
//
// https://www.sqlite.org/lang_transaction.html
func (c *Conn) BeginExclusive() (Tx, error) {
err := c.Exec(`BEGIN EXCLUSIVE`)
if err != nil {
return Tx{}, err
}
return Tx{c}, nil
}
// End calls either [Tx.Commit] or [Tx.Rollback]
// depending on whether *error points to a nil or non-nil error.
//
// This is meant to be deferred:
//
// func doWork(conn *sqlite3.Conn) (err error) {
// tx := conn.Begin()
// defer tx.End(&err)
//
// // ... do work in the transaction
// }
//
// https://www.sqlite.org/lang_transaction.html
func (tx Tx) End(errp *error) {
recovered := recover()
if recovered != nil {
defer panic(recovered)
}
if (errp == nil || *errp == nil) && recovered == nil {
// Success path.
if tx.c.GetAutocommit() { // There is nothing to commit.
return
}
*errp = tx.Commit()
if *errp == nil {
return
}
// Fall through to the error path.
}
// Error path.
if tx.c.GetAutocommit() { // There is nothing to rollback.
return
}
err := tx.Rollback()
if err != nil {
panic(err)
}
}
// Commit commits the transaction.
//
// https://www.sqlite.org/lang_transaction.html
func (tx Tx) Commit() error {
return tx.c.Exec(`COMMIT`)
}
// Rollback rolls back the transaction,
// even if the connection has been interrupted.
//
// https://www.sqlite.org/lang_transaction.html
func (tx Tx) Rollback() error {
return tx.c.txExecInterrupted(`ROLLBACK`)
}
// Savepoint is a marker within a transaction
// that allows for partial rollback.
//
// https://www.sqlite.org/lang_savepoint.html
type Savepoint struct {
c *Conn
name string
}
// Savepoint establishes a new transaction savepoint.
//
// https://www.sqlite.org/lang_savepoint.html
func (c *Conn) Savepoint() Savepoint {
name := "sqlite3.Savepoint"
var pc [1]uintptr
if n := runtime.Callers(2, pc[:]); n > 0 {
frames := runtime.CallersFrames(pc[:n])
frame, _ := frames.Next()
if frame.Function != "" {
name = frame.Function
}
}
// Names can be reused; this makes catching bugs more likely.
name += "#" + strconv.Itoa(int(rand.Int31()))
err := c.txExecInterrupted(fmt.Sprintf("SAVEPOINT %q;", name))
if err != nil {
panic(err)
}
return Savepoint{c: c, name: name}
}
// Release releases the savepoint rolling back any changes
// if *error points to a non-nil error.
//
// This is meant to be deferred:
//
// func doWork(conn *sqlite3.Conn) (err error) {
// savept := conn.Savepoint()
// defer savept.Release(&err)
//
// // ... do work in the transaction
// }
func (s Savepoint) Release(errp *error) {
recovered := recover()
if recovered != nil {
defer panic(recovered)
}
if (errp == nil || *errp == nil) && recovered == nil {
// Success path.
if s.c.GetAutocommit() { // There is nothing to commit.
return
}
*errp = s.c.Exec(fmt.Sprintf("RELEASE %q;", s.name))
if *errp == nil {
return
}
// Fall through to the error path.
}
// Error path.
if s.c.GetAutocommit() { // There is nothing to rollback.
return
}
// ROLLBACK and RELEASE even if interrupted.
err := s.c.txExecInterrupted(fmt.Sprintf(`
ROLLBACK TO %[1]q;
RELEASE %[1]q;
`, s.name))
if err != nil {
panic(err)
}
}
// Rollback rolls the transaction back to the savepoint,
// even if the connection has been interrupted.
// Rollback does not release the savepoint.
//
// https://www.sqlite.org/lang_transaction.html
func (s Savepoint) Rollback() error {
// ROLLBACK even if interrupted.
return s.c.txExecInterrupted(fmt.Sprintf("ROLLBACK TO %q;", s.name))
}
func (c *Conn) txExecInterrupted(sql string) error {
err := c.Exec(sql)
if errors.Is(err, INTERRUPT) {
old := c.SetInterrupt(context.Background())
defer c.SetInterrupt(old)
err = c.Exec(sql)
}
return err
}
+418
View File
@@ -0,0 +1,418 @@
package sqlite3
import (
"context"
"crypto/rand"
"errors"
"io"
"io/fs"
"os"
"path/filepath"
"runtime"
"time"
"github.com/ncruces/julianday"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
)
func vfsInstantiate(ctx context.Context, r wazero.Runtime) {
env := vfsNewEnvModuleBuilder(r)
_, err := env.Instantiate(ctx)
if err != nil {
panic(err)
}
}
func vfsNewEnvModuleBuilder(r wazero.Runtime) wazero.HostModuleBuilder {
env := r.NewHostModuleBuilder("env")
vfsRegisterFuncT(env, "os_localtime", vfsLocaltime)
vfsRegisterFunc3(env, "os_randomness", vfsRandomness)
vfsRegisterFunc2(env, "os_sleep", vfsSleep)
vfsRegisterFunc2(env, "os_current_time", vfsCurrentTime)
vfsRegisterFunc2(env, "os_current_time_64", vfsCurrentTime64)
vfsRegisterFunc4(env, "os_full_pathname", vfsFullPathname)
vfsRegisterFunc3(env, "os_delete", vfsDelete)
vfsRegisterFunc4(env, "os_access", vfsAccess)
vfsRegisterFunc5(env, "os_open", vfsOpen)
vfsRegisterFunc1(env, "os_close", vfsClose)
vfsRegisterFuncRW(env, "os_read", vfsRead)
vfsRegisterFuncRW(env, "os_write", vfsWrite)
vfsRegisterFuncT(env, "os_truncate", vfsTruncate)
vfsRegisterFunc2(env, "os_sync", vfsSync)
vfsRegisterFunc2(env, "os_file_size", vfsFileSize)
vfsRegisterFunc2(env, "os_lock", vfsLock)
vfsRegisterFunc2(env, "os_unlock", vfsUnlock)
vfsRegisterFunc2(env, "os_check_reserved_lock", vfsCheckReservedLock)
vfsRegisterFunc3(env, "os_file_control", vfsFileControl)
return env
}
// Poor man's namespaces.
const (
vfsOS vfsOSMethods = false
vfsFile vfsFileMethods = false
)
type (
vfsOSMethods bool
vfsFileMethods bool
)
type vfsKey struct{}
type vfsState struct {
files []*os.File
}
func vfsContext(ctx context.Context) (context.Context, io.Closer) {
vfs := &vfsState{}
return context.WithValue(ctx, vfsKey{}, vfs), vfs
}
func (vfs *vfsState) Close() error {
for _, f := range vfs.files {
if f != nil {
f.Close()
}
}
vfs.files = nil
return nil
}
func vfsLocaltime(ctx context.Context, mod api.Module, pTm uint32, t uint64) uint32 {
tm := time.Unix(int64(t), 0)
var isdst int
if tm.IsDST() {
isdst = 1
}
// https://pubs.opengroup.org/onlinepubs/7908799/xsh/time.h.html
mem := memory{mod}
mem.writeUint32(pTm+0*ptrlen, uint32(tm.Second()))
mem.writeUint32(pTm+1*ptrlen, uint32(tm.Minute()))
mem.writeUint32(pTm+2*ptrlen, uint32(tm.Hour()))
mem.writeUint32(pTm+3*ptrlen, uint32(tm.Day()))
mem.writeUint32(pTm+4*ptrlen, uint32(tm.Month()-time.January))
mem.writeUint32(pTm+5*ptrlen, uint32(tm.Year()-1900))
mem.writeUint32(pTm+6*ptrlen, uint32(tm.Weekday()-time.Sunday))
mem.writeUint32(pTm+7*ptrlen, uint32(tm.YearDay()-1))
mem.writeUint32(pTm+8*ptrlen, uint32(isdst))
return _OK
}
func vfsRandomness(ctx context.Context, mod api.Module, pVfs, nByte, zByte uint32) uint32 {
mem := memory{mod}.view(zByte, uint64(nByte))
n, _ := rand.Reader.Read(mem)
return uint32(n)
}
func vfsSleep(ctx context.Context, mod api.Module, pVfs, nMicro uint32) uint32 {
time.Sleep(time.Duration(nMicro) * time.Microsecond)
return _OK
}
func vfsCurrentTime(ctx context.Context, mod api.Module, pVfs, prNow uint32) uint32 {
day := julianday.Float(time.Now())
memory{mod}.writeFloat64(prNow, day)
return _OK
}
func vfsCurrentTime64(ctx context.Context, mod api.Module, pVfs, piNow uint32) uint32 {
day, nsec := julianday.Date(time.Now())
msec := day*86_400_000 + nsec/1_000_000
memory{mod}.writeUint64(piNow, uint64(msec))
return _OK
}
func vfsFullPathname(ctx context.Context, mod api.Module, pVfs, zRelative, nFull, zFull uint32) uint32 {
rel := memory{mod}.readString(zRelative, _MAX_PATHNAME)
abs, err := filepath.Abs(rel)
if err != nil {
return uint32(CANTOPEN_FULLPATH)
}
size := uint64(len(abs) + 1)
if size > uint64(nFull) {
return uint32(CANTOPEN_FULLPATH)
}
mem := memory{mod}.view(zFull, size)
mem[len(abs)] = 0
copy(mem, abs)
if fi, err := os.Lstat(abs); err == nil {
if fi.Mode()&fs.ModeSymlink != 0 {
return _OK_SYMLINK
}
return _OK
} else if errors.Is(err, fs.ErrNotExist) {
return _OK
}
return uint32(CANTOPEN_FULLPATH)
}
func vfsDelete(ctx context.Context, mod api.Module, pVfs, zPath, syncDir uint32) uint32 {
path := memory{mod}.readString(zPath, _MAX_PATHNAME)
err := os.Remove(path)
if errors.Is(err, fs.ErrNotExist) {
return uint32(IOERR_DELETE_NOENT)
}
if err != nil {
return uint32(IOERR_DELETE)
}
if runtime.GOOS != "windows" && syncDir != 0 {
f, err := os.Open(filepath.Dir(path))
if err != nil {
return _OK
}
defer f.Close()
err = vfsOS.Sync(f, false, false)
if err != nil {
return uint32(IOERR_DIR_FSYNC)
}
}
return _OK
}
func vfsAccess(ctx context.Context, mod api.Module, pVfs, zPath uint32, flags _AccessFlag, pResOut uint32) uint32 {
path := memory{mod}.readString(zPath, _MAX_PATHNAME)
err := vfsOS.Access(path, flags)
var res uint32
var rc xErrorCode
if flags == _ACCESS_EXISTS {
switch {
case err == nil:
res = 1
case errors.Is(err, fs.ErrNotExist):
res = 0
default:
rc = IOERR_ACCESS
}
} else {
switch {
case err == nil:
res = 1
case errors.Is(err, fs.ErrPermission):
res = 0
default:
rc = IOERR_ACCESS
}
}
memory{mod}.writeUint32(pResOut, res)
return uint32(rc)
}
func vfsOpen(ctx context.Context, mod api.Module, pVfs, zName, pFile uint32, flags OpenFlag, pOutFlags uint32) uint32 {
var oflags int
if flags&OPEN_EXCLUSIVE != 0 {
oflags |= os.O_EXCL
}
if flags&OPEN_CREATE != 0 {
oflags |= os.O_CREATE
}
if flags&OPEN_READONLY != 0 {
oflags |= os.O_RDONLY
}
if flags&OPEN_READWRITE != 0 {
oflags |= os.O_RDWR
}
var err error
var file *os.File
if zName == 0 {
file, err = os.CreateTemp("", "*.db")
} else {
name := memory{mod}.readString(zName, _MAX_PATHNAME)
file, err = vfsOS.OpenFile(name, oflags, 0600)
}
if err != nil {
return uint32(CANTOPEN)
}
if flags&OPEN_DELETEONCLOSE != 0 {
os.Remove(file.Name())
}
vfsFile.Open(ctx, mod, pFile, file)
if pOutFlags != 0 {
memory{mod}.writeUint32(pOutFlags, uint32(flags))
}
return _OK
}
func vfsClose(ctx context.Context, mod api.Module, pFile uint32) uint32 {
err := vfsFile.Close(ctx, mod, pFile)
if err != nil {
return uint32(IOERR_CLOSE)
}
return _OK
}
func vfsRead(ctx context.Context, mod api.Module, pFile, zBuf, iAmt uint32, iOfst uint64) uint32 {
buf := memory{mod}.view(zBuf, uint64(iAmt))
file := vfsFile.GetOS(ctx, mod, pFile)
n, err := file.ReadAt(buf, int64(iOfst))
if n == int(iAmt) {
return _OK
}
if n == 0 && err != io.EOF {
return uint32(IOERR_READ)
}
for i := range buf[n:] {
buf[n+i] = 0
}
return uint32(IOERR_SHORT_READ)
}
func vfsWrite(ctx context.Context, mod api.Module, pFile, zBuf, iAmt uint32, iOfst uint64) uint32 {
buf := memory{mod}.view(zBuf, uint64(iAmt))
file := vfsFile.GetOS(ctx, mod, pFile)
_, err := file.WriteAt(buf, int64(iOfst))
if err != nil {
return uint32(IOERR_WRITE)
}
return _OK
}
func vfsTruncate(ctx context.Context, mod api.Module, pFile uint32, nByte uint64) uint32 {
file := vfsFile.GetOS(ctx, mod, pFile)
err := file.Truncate(int64(nByte))
if err != nil {
return uint32(IOERR_TRUNCATE)
}
return _OK
}
func vfsSync(ctx context.Context, mod api.Module, pFile uint32, flags _SyncFlag) uint32 {
dataonly := (flags & _SYNC_DATAONLY) != 0
fullsync := (flags & 0x0f) == _SYNC_FULL
file := vfsFile.GetOS(ctx, mod, pFile)
err := vfsOS.Sync(file, fullsync, dataonly)
if err != nil {
return uint32(IOERR_FSYNC)
}
return _OK
}
func vfsFileSize(ctx context.Context, mod api.Module, pFile, pSize uint32) uint32 {
file := vfsFile.GetOS(ctx, mod, pFile)
off, err := file.Seek(0, io.SeekEnd)
if err != nil {
return uint32(IOERR_SEEK)
}
memory{mod}.writeUint64(pSize, uint64(off))
return _OK
}
func vfsFileControl(ctx context.Context, mod api.Module, pFile uint32, op _FcntlOpcode, pArg uint32) uint32 {
switch op {
case _FCNTL_SIZE_HINT:
return vfsSizeHint(ctx, mod, pFile, pArg)
case _FCNTL_HAS_MOVED:
return vfsFileMoved(ctx, mod, pFile, pArg)
}
return uint32(NOTFOUND)
}
func vfsSizeHint(ctx context.Context, mod api.Module, pFile, pArg uint32) uint32 {
file := vfsFile.GetOS(ctx, mod, pFile)
size := memory{mod}.readUint64(pArg)
err := vfsOS.Allocate(file, int64(size))
if err != nil {
return uint32(IOERR_TRUNCATE)
}
return _OK
}
func vfsFileMoved(ctx context.Context, mod api.Module, pFile, pResOut uint32) uint32 {
file := vfsFile.GetOS(ctx, mod, pFile)
fi, err := file.Stat()
if err != nil {
return uint32(IOERR_FSTAT)
}
pi, err := os.Stat(file.Name())
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return uint32(IOERR_FSTAT)
}
var res uint32
if !os.SameFile(fi, pi) {
res = 1
}
memory{mod}.writeUint32(pResOut, res)
return _OK
}
func vfsRegisterFunc1(mod wazero.HostModuleBuilder, name string, fn func(ctx context.Context, mod api.Module, _ uint32) uint32) {
mod.NewFunctionBuilder().
WithGoModuleFunction(api.GoModuleFunc(
func(ctx context.Context, mod api.Module, stack []uint64) {
stack[0] = uint64(fn(ctx, mod, uint32(stack[0])))
}),
[]api.ValueType{api.ValueTypeI32}, []api.ValueType{api.ValueTypeI32}).
Export(name)
}
func vfsRegisterFunc2[T0, T1 ~uint32](mod wazero.HostModuleBuilder, name string, fn func(ctx context.Context, mod api.Module, _ T0, _ T1) uint32) {
mod.NewFunctionBuilder().
WithGoModuleFunction(api.GoModuleFunc(
func(ctx context.Context, mod api.Module, stack []uint64) {
stack[0] = uint64(fn(ctx, mod, T0(stack[0]), T1(stack[1])))
}),
[]api.ValueType{api.ValueTypeI32, api.ValueTypeI32}, []api.ValueType{api.ValueTypeI32}).
Export(name)
}
func vfsRegisterFunc3[T0, T1, T2 ~uint32](mod wazero.HostModuleBuilder, name string, fn func(ctx context.Context, mod api.Module, _ T0, _ T1, _ T2) uint32) {
mod.NewFunctionBuilder().
WithGoModuleFunction(api.GoModuleFunc(
func(ctx context.Context, mod api.Module, stack []uint64) {
stack[0] = uint64(fn(ctx, mod, T0(stack[0]), T1(stack[1]), T2(stack[2])))
}),
[]api.ValueType{api.ValueTypeI32, api.ValueTypeI32, api.ValueTypeI32}, []api.ValueType{api.ValueTypeI32}).
Export(name)
}
func vfsRegisterFunc4[T0, T1, T2, T3 ~uint32](mod wazero.HostModuleBuilder, name string, fn func(ctx context.Context, mod api.Module, _ T0, _ T1, _ T2, _ T3) uint32) {
mod.NewFunctionBuilder().
WithGoModuleFunction(api.GoModuleFunc(
func(ctx context.Context, mod api.Module, stack []uint64) {
stack[0] = uint64(fn(ctx, mod, T0(stack[0]), T1(stack[1]), T2(stack[2]), T3(stack[3])))
}),
[]api.ValueType{api.ValueTypeI32, api.ValueTypeI32, api.ValueTypeI32, api.ValueTypeI32}, []api.ValueType{api.ValueTypeI32}).
Export(name)
}
func vfsRegisterFunc5[T0, T1, T2, T3, T4 ~uint32](mod wazero.HostModuleBuilder, name string, fn func(ctx context.Context, mod api.Module, _ T0, _ T1, _ T2, _ T3, _ T4) uint32) {
mod.NewFunctionBuilder().
WithGoModuleFunction(api.GoModuleFunc(
func(ctx context.Context, mod api.Module, stack []uint64) {
stack[0] = uint64(fn(ctx, mod, T0(stack[0]), T1(stack[1]), T2(stack[2]), T3(stack[3]), T4(stack[4])))
}),
[]api.ValueType{api.ValueTypeI32, api.ValueTypeI32, api.ValueTypeI32, api.ValueTypeI32, api.ValueTypeI32}, []api.ValueType{api.ValueTypeI32}).
Export(name)
}
func vfsRegisterFuncRW(mod wazero.HostModuleBuilder, name string, fn func(ctx context.Context, mod api.Module, _, _, _ uint32, _ uint64) uint32) {
mod.NewFunctionBuilder().
WithGoModuleFunction(api.GoModuleFunc(
func(ctx context.Context, mod api.Module, stack []uint64) {
stack[0] = uint64(fn(ctx, mod, uint32(stack[0]), uint32(stack[1]), uint32(stack[2]), stack[3]))
}),
[]api.ValueType{api.ValueTypeI32, api.ValueTypeI32, api.ValueTypeI32, api.ValueTypeI64}, []api.ValueType{api.ValueTypeI32}).
Export(name)
}
func vfsRegisterFuncT(mod wazero.HostModuleBuilder, name string, fn func(ctx context.Context, mod api.Module, _ uint32, _ uint64) uint32) {
mod.NewFunctionBuilder().
WithGoModuleFunction(api.GoModuleFunc(
func(ctx context.Context, mod api.Module, stack []uint64) {
stack[0] = uint64(fn(ctx, mod, uint32(stack[0]), stack[1]))
}),
[]api.ValueType{api.ValueTypeI32, api.ValueTypeI64}, []api.ValueType{api.ValueTypeI32}).
Export(name)
}
+69
View File
@@ -0,0 +1,69 @@
package sqlite3
import (
"context"
"os"
"time"
"github.com/tetratelabs/wazero/api"
)
const (
// These need to match the offsets asserted in os.c
vfsFileIDOffset = 4
vfsFileLockOffset = 8
vfsFileLockTimeoutOffset = 12
)
func (vfsFileMethods) NewID(ctx context.Context, file *os.File) uint32 {
vfs := ctx.Value(vfsKey{}).(*vfsState)
// Find an empty slot.
for id, ptr := range vfs.files {
if ptr == nil {
vfs.files[id] = file
return uint32(id)
}
}
// Add a new slot.
vfs.files = append(vfs.files, file)
return uint32(len(vfs.files) - 1)
}
func (vfsFileMethods) Open(ctx context.Context, mod api.Module, pFile uint32, file *os.File) {
mem := memory{mod}
id := vfsFile.NewID(ctx, file)
mem.writeUint32(pFile+vfsFileIDOffset, id)
}
func (vfsFileMethods) Close(ctx context.Context, mod api.Module, pFile uint32) error {
mem := memory{mod}
id := mem.readUint32(pFile + vfsFileIDOffset)
vfs := ctx.Value(vfsKey{}).(*vfsState)
file := vfs.files[id]
vfs.files[id] = nil
return file.Close()
}
func (vfsFileMethods) GetOS(ctx context.Context, mod api.Module, pFile uint32) *os.File {
mem := memory{mod}
id := mem.readUint32(pFile + vfsFileIDOffset)
vfs := ctx.Value(vfsKey{}).(*vfsState)
return vfs.files[id]
}
func (vfsFileMethods) GetLock(ctx context.Context, mod api.Module, pFile uint32) vfsLockState {
mem := memory{mod}
return vfsLockState(mem.readUint8(pFile + vfsFileLockOffset))
}
func (vfsFileMethods) SetLock(ctx context.Context, mod api.Module, pFile uint32, lock vfsLockState) {
mem := memory{mod}
mem.writeUint8(pFile+vfsFileLockOffset, uint8(lock))
}
func (vfsFileMethods) GetLockTimeout(ctx context.Context, mod api.Module, pFile uint32) time.Duration {
mem := memory{mod}
return time.Duration(mem.readUint32(pFile+vfsFileLockTimeoutOffset)) * time.Millisecond
}
+207
View File
@@ -0,0 +1,207 @@
package sqlite3
import (
"context"
"os"
"time"
"github.com/tetratelabs/wazero/api"
)
const (
// No locks are held on the database.
// The database may be neither read nor written.
// Any internally cached data is considered suspect and subject to
// verification against the database file before being used.
// Other processes can read or write the database as their own locking
// states permit.
// This is the default state.
_NO_LOCK = 0
// The database may be read but not written.
// Any number of processes can hold SHARED locks at the same time,
// hence there can be many simultaneous readers.
// But no other thread or process is allowed to write to the database file
// while one or more SHARED locks are active.
_SHARED_LOCK = 1
// A RESERVED lock means that the process is planning on writing to the
// database file at some point in the future but that it is currently just
// reading from the file.
// Only a single RESERVED lock may be active at one time,
// though multiple SHARED locks can coexist with a single RESERVED lock.
// RESERVED differs from PENDING in that new SHARED locks can be acquired
// while there is a RESERVED lock.
_RESERVED_LOCK = 2
// A PENDING lock means that the process holding the lock wants to write to
// the database as soon as possible and is just waiting on all current
// SHARED locks to clear so that it can get an EXCLUSIVE lock.
// No new SHARED locks are permitted against the database if a PENDING lock
// is active, though existing SHARED locks are allowed to continue.
_PENDING_LOCK = 3
// An EXCLUSIVE lock is needed in order to write to the database file.
// Only one EXCLUSIVE lock is allowed on the file and no other locks of any
// kind are allowed to coexist with an EXCLUSIVE lock.
// In order to maximize concurrency, SQLite works to minimize the amount of
// time that EXCLUSIVE locks are held.
_EXCLUSIVE_LOCK = 4
_PENDING_BYTE = 0x40000000
_RESERVED_BYTE = (_PENDING_BYTE + 1)
_SHARED_FIRST = (_PENDING_BYTE + 2)
_SHARED_SIZE = 510
)
type vfsLockState uint32
func vfsLock(ctx context.Context, mod api.Module, pFile uint32, eLock vfsLockState) uint32 {
// Argument check. SQLite never explicitly requests a pending lock.
if eLock != _SHARED_LOCK && eLock != _RESERVED_LOCK && eLock != _EXCLUSIVE_LOCK {
panic(assertErr())
}
file := vfsFile.GetOS(ctx, mod, pFile)
cLock := vfsFile.GetLock(ctx, mod, pFile)
timeout := vfsFile.GetLockTimeout(ctx, mod, pFile)
switch {
case cLock < _NO_LOCK || cLock > _EXCLUSIVE_LOCK:
// Connection state check.
panic(assertErr())
case cLock == _NO_LOCK && eLock > _SHARED_LOCK:
// We never move from unlocked to anything higher than a shared lock.
panic(assertErr())
case cLock != _SHARED_LOCK && eLock == _RESERVED_LOCK:
// A shared lock is always held when a reserved lock is requested.
panic(assertErr())
}
// If we already have an equal or more restrictive lock, do nothing.
if cLock >= eLock {
return _OK
}
switch eLock {
case _SHARED_LOCK:
// Must be unlocked to get SHARED.
if cLock != _NO_LOCK {
panic(assertErr())
}
if rc := vfsOS.GetSharedLock(file, timeout); rc != _OK {
return uint32(rc)
}
vfsFile.SetLock(ctx, mod, pFile, _SHARED_LOCK)
return _OK
case _RESERVED_LOCK:
// Must be SHARED to get RESERVED.
if cLock != _SHARED_LOCK {
panic(assertErr())
}
if rc := vfsOS.GetReservedLock(file, timeout); rc != _OK {
return uint32(rc)
}
vfsFile.SetLock(ctx, mod, pFile, _RESERVED_LOCK)
return _OK
case _EXCLUSIVE_LOCK:
// Must be SHARED, RESERVED or PENDING to get EXCLUSIVE.
if cLock <= _NO_LOCK || cLock >= _EXCLUSIVE_LOCK {
panic(assertErr())
}
// A PENDING lock is needed before acquiring an EXCLUSIVE lock.
if cLock < _PENDING_LOCK {
if rc := vfsOS.GetPendingLock(file); rc != _OK {
return uint32(rc)
}
vfsFile.SetLock(ctx, mod, pFile, _PENDING_LOCK)
}
if rc := vfsOS.GetExclusiveLock(file, timeout); rc != _OK {
return uint32(rc)
}
vfsFile.SetLock(ctx, mod, pFile, _EXCLUSIVE_LOCK)
return _OK
default:
panic(assertErr())
}
}
func vfsUnlock(ctx context.Context, mod api.Module, pFile uint32, eLock vfsLockState) uint32 {
// Argument check.
if eLock != _NO_LOCK && eLock != _SHARED_LOCK {
panic(assertErr())
}
file := vfsFile.GetOS(ctx, mod, pFile)
cLock := vfsFile.GetLock(ctx, mod, pFile)
// Connection state check.
if cLock < _NO_LOCK || cLock > _EXCLUSIVE_LOCK {
panic(assertErr())
}
// If we don't have a more restrictive lock, do nothing.
if cLock <= eLock {
return _OK
}
switch eLock {
case _SHARED_LOCK:
if rc := vfsOS.DowngradeLock(file, cLock); rc != _OK {
return uint32(rc)
}
vfsFile.SetLock(ctx, mod, pFile, _SHARED_LOCK)
return _OK
case _NO_LOCK:
rc := vfsOS.ReleaseLock(file, cLock)
vfsFile.SetLock(ctx, mod, pFile, _NO_LOCK)
return uint32(rc)
default:
panic(assertErr())
}
}
func vfsCheckReservedLock(ctx context.Context, mod api.Module, pFile, pResOut uint32) uint32 {
file := vfsFile.GetOS(ctx, mod, pFile)
cLock := vfsFile.GetLock(ctx, mod, pFile)
// Connection state check.
if cLock < _NO_LOCK || cLock > _EXCLUSIVE_LOCK {
panic(assertErr())
}
var locked bool
var rc xErrorCode
if cLock >= _RESERVED_LOCK {
locked = true
} else {
locked, rc = vfsOS.CheckReservedLock(file)
}
var res uint32
if locked {
res = 1
}
memory{mod}.writeUint32(pResOut, res)
return uint32(rc)
}
func (vfsOSMethods) GetReservedLock(file *os.File, timeout time.Duration) xErrorCode {
// Acquire the RESERVED lock.
return vfsOS.writeLock(file, _RESERVED_BYTE, 1, timeout)
}
func (vfsOSMethods) GetPendingLock(file *os.File) xErrorCode {
// Acquire the PENDING lock.
return vfsOS.writeLock(file, _PENDING_BYTE, 1, 0)
}
func (vfsOSMethods) CheckReservedLock(file *os.File) (bool, xErrorCode) {
// Test the RESERVED lock.
return vfsOS.checkLock(file, _RESERVED_BYTE, 1)
}
+56
View File
@@ -0,0 +1,56 @@
//go:build freebsd || openbsd || netbsd || dragonfly || (darwin && sqlite3_bsd)
package sqlite3
import (
"os"
"time"
"golang.org/x/sys/unix"
)
func (vfsOSMethods) unlock(file *os.File, start, len int64) xErrorCode {
if start == 0 && len == 0 {
err := unix.Flock(int(file.Fd()), unix.LOCK_UN)
if err != nil {
return IOERR_UNLOCK
}
}
return _OK
}
func (vfsOSMethods) lock(file *os.File, how int, timeout time.Duration, def xErrorCode) xErrorCode {
var err error
for {
err = unix.Flock(int(file.Fd()), how)
if errno, _ := err.(unix.Errno); errno != unix.EAGAIN {
break
}
if timeout < time.Millisecond {
break
}
timeout -= time.Millisecond
time.Sleep(time.Millisecond)
}
return vfsOS.lockErrorCode(err, def)
}
func (vfsOSMethods) readLock(file *os.File, start, len int64, timeout time.Duration) xErrorCode {
return vfsOS.lock(file, unix.LOCK_SH|unix.LOCK_NB, timeout, IOERR_RDLOCK)
}
func (vfsOSMethods) writeLock(file *os.File, start, len int64, timeout time.Duration) xErrorCode {
return vfsOS.lock(file, unix.LOCK_EX|unix.LOCK_NB, timeout, IOERR_LOCK)
}
func (vfsOSMethods) checkLock(file *os.File, start, len int64) (bool, xErrorCode) {
lock := unix.Flock_t{
Type: unix.F_RDLCK,
Start: start,
Len: len,
}
if unix.FcntlFlock(file.Fd(), unix.F_GETLK, &lock) != nil {
return false, IOERR_CHECKRESERVEDLOCK
}
return lock.Type != unix.F_UNLCK, _OK
}
+106
View File
@@ -0,0 +1,106 @@
//go:build !sqlite3_bsd
package sqlite3
import (
"io"
"os"
"time"
"golang.org/x/sys/unix"
)
const (
// https://github.com/apple/darwin-xnu/blob/main/bsd/sys/fcntl.h
_F_OFD_SETLK = 90
_F_OFD_SETLKW = 91
_F_OFD_GETLK = 92
_F_OFD_SETLKWTIMEOUT = 93
)
type flocktimeout_t struct {
fl unix.Flock_t
timeout unix.Timespec
}
func (vfsOSMethods) Sync(file *os.File, fullsync, dataonly bool) error {
if fullsync {
return file.Sync()
}
return unix.Fsync(int(file.Fd()))
}
func (vfsOSMethods) Allocate(file *os.File, size int64) error {
off, err := file.Seek(0, io.SeekEnd)
if err != nil {
return err
}
if size <= off {
return nil
}
// https://stackoverflow.com/a/11497568/867786
store := unix.Fstore_t{
Flags: unix.F_ALLOCATECONTIG,
Posmode: unix.F_PEOFPOSMODE,
Offset: 0,
Length: size,
}
// Try to get a continous chunk of disk space.
err = unix.FcntlFstore(file.Fd(), unix.F_PREALLOCATE, &store)
if err != nil {
// OK, perhaps we are too fragmented, allocate non-continuous.
store.Flags = unix.F_ALLOCATEALL
unix.FcntlFstore(file.Fd(), unix.F_PREALLOCATE, &store)
}
return file.Truncate(size)
}
func (vfsOSMethods) unlock(file *os.File, start, len int64) xErrorCode {
err := unix.FcntlFlock(file.Fd(), _F_OFD_SETLK, &unix.Flock_t{
Type: unix.F_UNLCK,
Start: start,
Len: len,
})
if err != nil {
return IOERR_UNLOCK
}
return _OK
}
func (vfsOSMethods) lock(file *os.File, typ int16, start, len int64, timeout time.Duration, def xErrorCode) xErrorCode {
lock := flocktimeout_t{fl: unix.Flock_t{
Type: typ,
Start: start,
Len: len,
}}
var err error
if timeout == 0 {
err = unix.FcntlFlock(file.Fd(), _F_OFD_SETLK, &lock.fl)
} else {
lock.timeout = unix.NsecToTimespec(int64(timeout / time.Nanosecond))
err = unix.FcntlFlock(file.Fd(), _F_OFD_SETLKWTIMEOUT, &lock.fl)
}
return vfsOS.lockErrorCode(err, def)
}
func (vfsOSMethods) readLock(file *os.File, start, len int64, timeout time.Duration) xErrorCode {
return vfsOS.lock(file, unix.F_RDLCK, start, len, timeout, IOERR_RDLOCK)
}
func (vfsOSMethods) writeLock(file *os.File, start, len int64, timeout time.Duration) xErrorCode {
return vfsOS.lock(file, unix.F_WRLCK, start, len, timeout, IOERR_LOCK)
}
func (vfsOSMethods) checkLock(file *os.File, start, len int64) (bool, xErrorCode) {
lock := unix.Flock_t{
Type: unix.F_RDLCK,
Start: start,
Len: len,
}
if unix.FcntlFlock(file.Fd(), _F_OFD_GETLK, &lock) != nil {
return false, IOERR_CHECKRESERVEDLOCK
}
return lock.Type != unix.F_UNLCK, _OK
}
+25
View File
@@ -0,0 +1,25 @@
package sqlite3
import (
"os"
"golang.org/x/sys/unix"
)
func (vfsOSMethods) Sync(file *os.File, fullsync, dataonly bool) error {
if dataonly {
_, _, err := unix.Syscall(unix.SYS_FDATASYNC, file.Fd(), 0, 0)
if err != 0 {
return err
}
return nil
}
return file.Sync()
}
func (vfsOSMethods) Allocate(file *os.File, size int64) error {
if size == 0 {
return nil
}
return unix.Fallocate(int(file.Fd()), 0, 0, size)
}
+63
View File
@@ -0,0 +1,63 @@
//go:build linux || illumos
package sqlite3
import (
"os"
"time"
"golang.org/x/sys/unix"
)
func (vfsOSMethods) unlock(file *os.File, start, len int64) xErrorCode {
err := unix.FcntlFlock(file.Fd(), unix.F_OFD_SETLK, &unix.Flock_t{
Type: unix.F_UNLCK,
Start: start,
Len: len,
})
if err != nil {
return IOERR_UNLOCK
}
return _OK
}
func (vfsOSMethods) lock(file *os.File, typ int16, start, len int64, timeout time.Duration, def xErrorCode) xErrorCode {
lock := unix.Flock_t{
Type: typ,
Start: start,
Len: len,
}
var err error
for {
err = unix.FcntlFlock(file.Fd(), unix.F_OFD_SETLK, &lock)
if errno, _ := err.(unix.Errno); errno != unix.EAGAIN {
break
}
if timeout < time.Millisecond {
break
}
timeout -= time.Millisecond
time.Sleep(time.Millisecond)
}
return vfsOS.lockErrorCode(err, def)
}
func (vfsOSMethods) readLock(file *os.File, start, len int64, timeout time.Duration) xErrorCode {
return vfsOS.lock(file, unix.F_RDLCK, start, len, timeout, IOERR_RDLOCK)
}
func (vfsOSMethods) writeLock(file *os.File, start, len int64, timeout time.Duration) xErrorCode {
return vfsOS.lock(file, unix.F_WRLCK, start, len, timeout, IOERR_LOCK)
}
func (vfsOSMethods) checkLock(file *os.File, start, len int64) (bool, xErrorCode) {
lock := unix.Flock_t{
Type: unix.F_RDLCK,
Start: start,
Len: len,
}
if unix.FcntlFlock(file.Fd(), unix.F_OFD_GETLK, &lock) != nil {
return false, IOERR_CHECKRESERVEDLOCK
}
return lock.Type != unix.F_UNLCK, _OK
}
+23
View File
@@ -0,0 +1,23 @@
//go:build !linux && (!darwin || sqlite3_bsd)
package sqlite3
import (
"io"
"os"
)
func (vfsOSMethods) Sync(file *os.File, fullsync, dataonly bool) error {
return file.Sync()
}
func (vfsOSMethods) Allocate(file *os.File, size int64) error {
off, err := file.Seek(0, io.SeekEnd)
if err != nil {
return err
}
if size <= off {
return nil
}
return file.Truncate(size)
}
+87
View File
@@ -0,0 +1,87 @@
//go:build unix
package sqlite3
import (
"io/fs"
"os"
"time"
"golang.org/x/sys/unix"
)
func (vfsOSMethods) OpenFile(name string, flag int, perm fs.FileMode) (*os.File, error) {
return os.OpenFile(name, flag, perm)
}
func (vfsOSMethods) Access(path string, flags _AccessFlag) error {
var access uint32 // unix.F_OK
switch flags {
case _ACCESS_READWRITE:
access = unix.R_OK | unix.W_OK
case _ACCESS_READ:
access = unix.R_OK
}
return unix.Access(path, access)
}
func (vfsOSMethods) GetSharedLock(file *os.File, timeout time.Duration) xErrorCode {
// Test the PENDING lock before acquiring a new SHARED lock.
if pending, _ := vfsOS.checkLock(file, _PENDING_BYTE, 1); pending {
return xErrorCode(BUSY)
}
// Acquire the SHARED lock.
return vfsOS.readLock(file, _SHARED_FIRST, _SHARED_SIZE, timeout)
}
func (vfsOSMethods) GetExclusiveLock(file *os.File, timeout time.Duration) xErrorCode {
if timeout == 0 {
timeout = time.Millisecond
}
// Acquire the EXCLUSIVE lock.
return vfsOS.writeLock(file, _SHARED_FIRST, _SHARED_SIZE, timeout)
}
func (vfsOSMethods) DowngradeLock(file *os.File, state vfsLockState) xErrorCode {
if state >= _EXCLUSIVE_LOCK {
// Downgrade to a SHARED lock.
if rc := vfsOS.readLock(file, _SHARED_FIRST, _SHARED_SIZE, 0); rc != _OK {
// In theory, the downgrade to a SHARED cannot fail because another
// process is holding an incompatible lock. If it does, this
// indicates that the other process is not following the locking
// protocol. If this happens, return IOERR_RDLOCK. Returning
// BUSY would confuse the upper layer.
return IOERR_RDLOCK
}
}
// Release the PENDING and RESERVED locks.
return vfsOS.unlock(file, _PENDING_BYTE, 2)
}
func (vfsOSMethods) ReleaseLock(file *os.File, _ vfsLockState) xErrorCode {
// Release all locks.
return vfsOS.unlock(file, 0, 0)
}
func (vfsOSMethods) lockErrorCode(err error, def xErrorCode) xErrorCode {
if err == nil {
return _OK
}
if errno, ok := err.(unix.Errno); ok {
switch errno {
case
unix.EACCES,
unix.EAGAIN,
unix.EBUSY,
unix.EINTR,
unix.ENOLCK,
unix.EDEADLK,
unix.ETIMEDOUT:
return xErrorCode(BUSY)
case unix.EPERM:
return xErrorCode(PERM)
}
}
return def
}
+240
View File
@@ -0,0 +1,240 @@
package sqlite3
import (
"io/fs"
"os"
"syscall"
"time"
"golang.org/x/sys/windows"
)
// OpenFile is a simplified copy of [os.openFileNolog]
// that uses syscall.FILE_SHARE_DELETE.
// https://go.dev/src/os/file_windows.go
//
// See: https://go.dev/issue/32088
func (vfsOSMethods) OpenFile(name string, flag int, perm fs.FileMode) (*os.File, error) {
if name == "" {
return nil, &os.PathError{Op: "open", Path: name, Err: syscall.ENOENT}
}
r, e := syscallOpen(name, flag, uint32(perm.Perm()))
if e != nil {
return nil, &os.PathError{Op: "open", Path: name, Err: e}
}
return os.NewFile(uintptr(r), name), nil
}
func (vfsOSMethods) Access(path string, flags _AccessFlag) error {
fi, err := os.Stat(path)
if err != nil {
return err
}
if flags == _ACCESS_EXISTS {
return nil
}
var want fs.FileMode = windows.S_IRUSR
if flags == _ACCESS_READWRITE {
want |= windows.S_IWUSR
}
if fi.IsDir() {
want |= windows.S_IXUSR
}
if fi.Mode()&want != want {
return fs.ErrPermission
}
return nil
}
func (vfsOSMethods) GetSharedLock(file *os.File, timeout time.Duration) xErrorCode {
// Acquire the PENDING lock temporarily before acquiring a new SHARED lock.
rc := vfsOS.readLock(file, _PENDING_BYTE, 1, timeout)
if rc == _OK {
// Acquire the SHARED lock.
rc = vfsOS.readLock(file, _SHARED_FIRST, _SHARED_SIZE, 0)
// Release the PENDING lock.
vfsOS.unlock(file, _PENDING_BYTE, 1)
}
return rc
}
func (vfsOSMethods) GetExclusiveLock(file *os.File, timeout time.Duration) xErrorCode {
if timeout == 0 {
timeout = time.Millisecond
}
// Release the SHARED lock.
vfsOS.unlock(file, _SHARED_FIRST, _SHARED_SIZE)
// Acquire the EXCLUSIVE lock.
rc := vfsOS.writeLock(file, _SHARED_FIRST, _SHARED_SIZE, timeout)
if rc != _OK {
// Reacquire the SHARED lock.
vfsOS.readLock(file, _SHARED_FIRST, _SHARED_SIZE, 0)
}
return rc
}
func (vfsOSMethods) DowngradeLock(file *os.File, state vfsLockState) xErrorCode {
if state >= _EXCLUSIVE_LOCK {
// Release the SHARED lock.
vfsOS.unlock(file, _SHARED_FIRST, _SHARED_SIZE)
// Reacquire the SHARED lock.
if rc := vfsOS.readLock(file, _SHARED_FIRST, _SHARED_SIZE, 0); rc != _OK {
// This should never happen.
// We should always be able to reacquire the read lock.
return IOERR_RDLOCK
}
}
// Release the PENDING and RESERVED locks.
if state >= _RESERVED_LOCK {
vfsOS.unlock(file, _RESERVED_BYTE, 1)
}
if state >= _PENDING_LOCK {
vfsOS.unlock(file, _PENDING_BYTE, 1)
}
return _OK
}
func (vfsOSMethods) ReleaseLock(file *os.File, state vfsLockState) xErrorCode {
// Release all locks.
if state >= _RESERVED_LOCK {
vfsOS.unlock(file, _RESERVED_BYTE, 1)
}
if state >= _SHARED_LOCK {
vfsOS.unlock(file, _SHARED_FIRST, _SHARED_SIZE)
}
if state >= _PENDING_LOCK {
vfsOS.unlock(file, _PENDING_BYTE, 1)
}
return _OK
}
func (vfsOSMethods) unlock(file *os.File, start, len uint32) xErrorCode {
err := windows.UnlockFileEx(windows.Handle(file.Fd()),
0, len, 0, &windows.Overlapped{Offset: start})
if err == windows.ERROR_NOT_LOCKED {
return _OK
}
if err != nil {
return IOERR_UNLOCK
}
return _OK
}
func (vfsOSMethods) lock(file *os.File, flags, start, len uint32, timeout time.Duration, def xErrorCode) xErrorCode {
var err error
for {
err = windows.LockFileEx(windows.Handle(file.Fd()), flags,
0, len, 0, &windows.Overlapped{Offset: start})
if errno, _ := err.(windows.Errno); errno != windows.ERROR_LOCK_VIOLATION {
break
}
if timeout < time.Millisecond {
break
}
timeout -= time.Millisecond
time.Sleep(time.Millisecond)
}
return vfsOS.lockErrorCode(err, def)
}
func (vfsOSMethods) readLock(file *os.File, start, len uint32, timeout time.Duration) xErrorCode {
return vfsOS.lock(file,
windows.LOCKFILE_FAIL_IMMEDIATELY,
start, len, timeout, IOERR_RDLOCK)
}
func (vfsOSMethods) writeLock(file *os.File, start, len uint32, timeout time.Duration) xErrorCode {
return vfsOS.lock(file,
windows.LOCKFILE_FAIL_IMMEDIATELY|windows.LOCKFILE_EXCLUSIVE_LOCK,
start, len, timeout, IOERR_LOCK)
}
func (vfsOSMethods) checkLock(file *os.File, start, len uint32) (bool, xErrorCode) {
rc := vfsOS.lock(file,
windows.LOCKFILE_FAIL_IMMEDIATELY,
start, len, 0, IOERR_CHECKRESERVEDLOCK)
if rc == xErrorCode(BUSY) {
return true, _OK
}
if rc == _OK {
vfsOS.unlock(file, start, len)
}
return false, rc
}
func (vfsOSMethods) lockErrorCode(err error, def xErrorCode) xErrorCode {
if err == nil {
return _OK
}
if errno, ok := err.(windows.Errno); ok {
// https://devblogs.microsoft.com/oldnewthing/20140905-00/?p=63
switch errno {
case
windows.ERROR_LOCK_VIOLATION,
windows.ERROR_IO_PENDING,
windows.ERROR_OPERATION_ABORTED:
return xErrorCode(BUSY)
}
}
return def
}
// syscallOpen is a simplified copy of [syscall.Open]
// that uses syscall.FILE_SHARE_DELETE.
// https://go.dev/src/syscall/syscall_windows.go
func syscallOpen(path string, mode int, perm uint32) (fd syscall.Handle, err error) {
if len(path) == 0 {
return syscall.InvalidHandle, syscall.ERROR_FILE_NOT_FOUND
}
pathp, err := syscall.UTF16PtrFromString(path)
if err != nil {
return syscall.InvalidHandle, err
}
var access uint32
switch mode & (syscall.O_RDONLY | syscall.O_WRONLY | syscall.O_RDWR) {
case syscall.O_RDONLY:
access = syscall.GENERIC_READ
case syscall.O_WRONLY:
access = syscall.GENERIC_WRITE
case syscall.O_RDWR:
access = syscall.GENERIC_READ | syscall.GENERIC_WRITE
}
if mode&syscall.O_CREAT != 0 {
access |= syscall.GENERIC_WRITE
}
if mode&syscall.O_APPEND != 0 {
access &^= syscall.GENERIC_WRITE
access |= syscall.FILE_APPEND_DATA
}
sharemode := uint32(syscall.FILE_SHARE_READ | syscall.FILE_SHARE_WRITE | syscall.FILE_SHARE_DELETE)
var createmode uint32
switch {
case mode&(syscall.O_CREAT|syscall.O_EXCL) == (syscall.O_CREAT | syscall.O_EXCL):
createmode = syscall.CREATE_NEW
case mode&(syscall.O_CREAT|syscall.O_TRUNC) == (syscall.O_CREAT | syscall.O_TRUNC):
createmode = syscall.CREATE_ALWAYS
case mode&syscall.O_CREAT == syscall.O_CREAT:
createmode = syscall.OPEN_ALWAYS
case mode&syscall.O_TRUNC == syscall.O_TRUNC:
createmode = syscall.TRUNCATE_EXISTING
default:
createmode = syscall.OPEN_EXISTING
}
var attrs uint32 = syscall.FILE_ATTRIBUTE_NORMAL
if perm&syscall.S_IWRITE == 0 {
attrs = syscall.FILE_ATTRIBUTE_READONLY
}
if createmode == syscall.OPEN_EXISTING && access == syscall.GENERIC_READ {
// Necessary for opening directory handles.
attrs |= syscall.FILE_FLAG_BACKUP_SEMANTICS
}
return syscall.CreateFile(pathp, access, sharemode, nil, createmode, attrs, 0)
}
+15
View File
@@ -0,0 +1,15 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Nuno Cruces
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+9
View File
@@ -0,0 +1,9 @@
# Julian Day calculator
[![Go Reference](https://pkg.go.dev/badge/image)](https://pkg.go.dev/github.com/ncruces/julianday)
[![Go Report](https://goreportcard.com/badge/github.com/ncruces/julianday)](https://goreportcard.com/report/github.com/ncruces/julianday)
[![Go Coverage](https://github.com/ncruces/julianday/wiki/coverage.svg)](https://raw.githack.com/wiki/ncruces/julianday/coverage.html)
https://en.wikipedia.org/wiki/Julian_day
Compatible with [SQLite](https://www.sqlite.org/lang_datefunc.html).
+124
View File
@@ -0,0 +1,124 @@
// Package julianday provides Time to Julian day conversions.
package julianday
import (
"bytes"
"errors"
"math"
"strconv"
"time"
)
const secs_per_day = 86_400
const nsec_per_sec = 1_000_000_000
const nsec_per_day = nsec_per_sec * secs_per_day
const epoch_days = 2_440_587
const epoch_secs = secs_per_day / 2
func jd(t time.Time) (day, nsec int64) {
sec := t.Unix()
// guaranteed not to overflow
day, sec = sec/secs_per_day+epoch_days, sec%secs_per_day+epoch_secs
return day, sec*nsec_per_sec + int64(t.Nanosecond())
}
// Date returns the Julian day number for t,
// and the nanosecond offset within that day,
// in the range [0, 86399999999999].
func Date(t time.Time) (day, nsec int64) {
day, nsec = jd(t)
switch {
case nsec < 0:
day -= 1
nsec += nsec_per_day
case nsec >= nsec_per_day:
day += 1
nsec -= nsec_per_day
}
return day, nsec
}
// Float returns the Julian date for t as a float64.
//
// In the XXI century, this has submillisecond precision.
func Float(t time.Time) float64 {
day, nsec := jd(t)
// converting day and nsec to float64 is exact
return float64(day) + float64(nsec)/nsec_per_day
}
// Float returns the Julian date for t as a string.
//
// This has nanosecond precision.
func Format(t time.Time) string {
var buf [32]byte
return string(AppendFormat(buf[:0], t))
}
// AppendFormat is like Format but appends the textual representation to dst
// and returns the extended buffer.
func AppendFormat(dst []byte, t time.Time) []byte {
day, nsec := Date(t)
if day < 0 && nsec != 0 {
dst = append(dst, '-')
day = ^day
nsec = nsec_per_day - nsec
}
var buf [20]byte
dst = strconv.AppendInt(dst, day, 10)
frac := strconv.AppendFloat(buf[:0], float64(nsec)/nsec_per_day, 'f', 15, 64)
return append(dst, bytes.TrimRight(frac[1:], ".0")...)
}
// Time returns the UTC Time corresponding to the Julian day number
// and nanosecond offset within that day.
// Not all day values have a corresponding time value.
func Time(day, nsec int64) time.Time {
return time.Unix((day-epoch_days)*secs_per_day-epoch_secs, nsec).UTC()
}
// FloatTime returns the UTC Time corresponding to a Julian date.
// Not all date values have a corresponding time value.
//
// In the XXI century, this has submillisecond precision.
func FloatTime(date float64) time.Time {
day, frac := math.Modf(date)
nsec := math.Floor(frac * nsec_per_day)
return Time(int64(day), int64(nsec))
}
// Parse parses a formatted Julian date and returns the time value it represents.
//
// This has nanosecond precision.
func Parse(s string) (time.Time, error) {
digits := 0
dot := len(s)
for i, b := range []byte(s) {
if '0' <= b && b <= '9' {
digits++
continue
}
if b == '.' && i < dot {
dot = i
continue
}
if (b == '+' || b == '-') && i == 0 {
continue
}
return time.Time{}, errors.New("julianday: invalid syntax")
}
if digits == 0 {
return time.Time{}, errors.New("julianday: invalid syntax")
}
day, err := strconv.ParseInt(s[:dot], 10, 64)
if err != nil && dot > 0 {
return time.Time{}, errors.New("julianday: value out of range")
}
frac, _ := strconv.ParseFloat(s[dot:], 64)
nsec := int64(math.Round(frac * nsec_per_day))
if s[0] == '-' {
nsec = -nsec
}
return Time(day, nsec), nil
}
+7
View File
@@ -0,0 +1,7 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
+2
View File
@@ -0,0 +1,2 @@
# Improves experience of commands like `make format` on Windows
* text=auto eol=lf
+41
View File
@@ -0,0 +1,41 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
/wazero
build
dist
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
# Goland
.idea
# AssemblyScript
node_modules
package-lock.json
# codecov.io
/coverage.txt
.vagrant
zig-cache/
zig-out/
.DS_Store
+3
View File
@@ -0,0 +1,3 @@
[submodule "site/themes/hello-friend"]
path = site/themes/hello-friend
url = https://github.com/panr/hugo-theme-hello-friend.git
+64
View File
@@ -0,0 +1,64 @@
# Contributing
We welcome contributions from the community. Please read the following guidelines carefully to maximize the chances of your PR being merged.
## Coding Style
- To ensure your change passes format checks, run `make check`. To format your files, you can run `make format`.
- We follow standard Go table-driven tests and use an internal [testing library](./internal/testing/require) to assert correctness. To verify all tests pass, you can run `make test`.
## DCO
We require DCO signoff line in every commit to this repo.
The sign-off is a simple line at the end of the explanation for the
patch, which certifies that you wrote it or otherwise have the right to
pass it on as an open-source patch. The rules are pretty simple: if you
can certify the below (from
[developercertificate.org](https://developercertificate.org/)):
```
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```
then you just add a line to every git commit message:
Signed-off-by: Joe Smith <joe@gmail.com>
using your real name (sorry, no pseudonyms or anonymous contributions.)
You can add the sign off when creating the git commit via `git commit -s`.
## Code Reviews
* A single approval is sufficient to merge. If a reviewer asks for changes in a PR they should be
addressed before the PR is merged, even if another reviewer has already approved the PR.
* During the review, address the comments and commit the changes _without_ squashing the commits.
This facilitates incremental reviews since the reviewer does not go through all the code again to
find out what has changed since the last review.
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2020-2021 wazero authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+346
View File
@@ -0,0 +1,346 @@
gofumpt := mvdan.cc/[email protected]
gosimports := github.com/rinchsan/gosimports/cmd/[email protected]
golangci_lint := github.com/golangci/golangci-lint/cmd/[email protected]
asmfmt := github.com/klauspost/asmfmt/cmd/[email protected]
# sync this with netlify.toml!
hugo := github.com/gohugoio/[email protected]
# Make 3.81 doesn't support '**' globbing: Set explicitly instead of recursion.
all_sources := $(wildcard *.go */*.go */*/*.go */*/*/*.go */*/*/*.go */*/*/*/*.go)
all_testdata := $(wildcard testdata/* */testdata/* */*/testdata/* */*/testdata/*/* */*/*/testdata/*)
all_testing := $(wildcard internal/testing/* internal/testing/*/* internal/testing/*/*/*)
all_examples := $(wildcard examples/* examples/*/* examples/*/*/* */*/example/* */*/example/*/* */*/example/*/*/*)
all_it := $(wildcard internal/integration_test/* internal/integration_test/*/* internal/integration_test/*/*/*)
# main_sources exclude any test or example related code
main_sources := $(wildcard $(filter-out %_test.go $(all_testdata) $(all_testing) $(all_examples) $(all_it), $(all_sources)))
# main_packages collect the unique main source directories (sort will dedupe).
# Paths need to all start with ./, so we do that manually vs foreach which strips it.
main_packages := $(sort $(foreach f,$(dir $(main_sources)),$(if $(findstring ./,$(f)),./,./$(f))))
# By default, we don't run with -race as it's costly to run on every PR.
go_test_options ?= -timeout 120s
ensureCompilerFastest := -ldflags '-X github.com/tetratelabs/wazero/internal/integration_test/vs.ensureCompilerFastest=true'
.PHONY: bench
bench:
@go test -run=NONE -benchmem -bench=. ./internal/integration_test/bench/...
@go test -benchmem -bench=. ./internal/integration_test/vs/... $(ensureCompilerFastest)
.PHONY: bench.check
bench.check:
@go build ./internal/integration_test/bench/...
@# Don't use -test.benchmem as it isn't accurate when comparing against CGO libs
@for d in vs/time vs/wasmedge vs/wasmer vs/wasmtime ; do \
cd ./internal/integration_test/$$d ; \
go test -bench=. . -tags='wasmedge' $(ensureCompilerFastest) ; \
cd - ;\
done
bench_testdata_dir := internal/integration_test/bench/testdata
.PHONY: build.bench
build.bench:
@tinygo build -o $(bench_testdata_dir)/case.wasm -scheduler=none --no-debug -target=wasi $(bench_testdata_dir)/case.go
.PHONY: test.examples
test.examples:
@go test $(go_test_options) ./examples/... ./imports/assemblyscript/example/... ./imports/emscripten/... ./experimental/gojs/example/... ./imports/wasi_snapshot_preview1/example/...
.PHONY: build.examples.as
build.examples.as:
@cd ./imports/assemblyscript/example/testdata && npm install && npm run build
%.wasm: %.zig
@(cd $(@D); zig build -Doptimize=ReleaseSmall)
@mv $(@D)/zig-out/*/$(@F) $(@D)
.PHONY: build.examples.zig
build.examples.zig: examples/allocation/zig/testdata/greet.wasm imports/wasi_snapshot_preview1/example/testdata/zig/cat.wasm imports/wasi_snapshot_preview1/testdata/zig/wasi.wasm
@cd internal/testing/dwarftestdata/testdata/zig; zig build; mv zig-out/*/main.wasm ./ # Need DWARF custom sections.
tinygo_sources := examples/basic/testdata/add.go examples/allocation/tinygo/testdata/greet.go examples/cli/testdata/cli.go imports/wasi_snapshot_preview1/example/testdata/tinygo/cat.go
.PHONY: build.examples.tinygo
build.examples.tinygo: $(tinygo_sources)
@for f in $^; do \
tinygo build -o $$(echo $$f | sed -e 's/\.go/\.wasm/') -scheduler=none --no-debug --target=wasi $$f; \
done
# We use zig to build C as it is easy to install and embeds a copy of zig-cc.
c_sources := imports/wasi_snapshot_preview1/example/testdata/zig-cc/cat.c imports/wasi_snapshot_preview1/testdata/zig-cc/wasi.c
.PHONY: build.examples.zig-cc
build.examples.zig-cc: $(c_sources)
@for f in $^; do \
zig cc --target=wasm32-wasi -Oz -o $$(echo $$f | sed -e 's/\.c/\.wasm/') $$f; \
done
# Here are the emcc args we use:
#
# * `-Oz` - most optimization for code size.
# * `--profiling` - adds the name section.
# * `-s STANDALONE_WASM` - ensures wasm is built for a non-js runtime.
# * `-s EXPORTED_FUNCTIONS=_malloc,_free` - export allocation functions so that
# they can be used externally as "malloc" and "free".
# * `-s WARN_ON_UNDEFINED_SYMBOLS=0` - imports not defined in JavaScript error
# otherwise. See https://github.com/emscripten-core/emscripten/issues/13641
# * `-s TOTAL_STACK=8KB -s TOTAL_MEMORY=64KB` - reduce memory default from 16MB
# to one page (64KB). To do this, we have to reduce the stack size.
# * `-s ALLOW_MEMORY_GROWTH` - allows "memory.grow" instructions to succeed, but
# requires a function import "emscripten_notify_memory_growth".
emscripten_sources := $(wildcard imports/emscripten/testdata/*.cc)
.PHONY: build.examples.emscripten
build.examples.emscripten: $(emscripten_sources)
@for f in $^; do \
em++ -Oz --profiling \
-s STANDALONE_WASM \
-s EXPORTED_FUNCTIONS=_malloc,_free \
-s WARN_ON_UNDEFINED_SYMBOLS=0 \
-s TOTAL_STACK=8KB -s TOTAL_MEMORY=64KB \
-s ALLOW_MEMORY_GROWTH \
--std=c++17 -o $$(echo $$f | sed -e 's/\.cc/\.wasm/') $$f; \
done
%/greet.wasm : cargo_target := wasm32-unknown-unknown
%/cat.wasm : cargo_target := wasm32-wasi
%/wasi.wasm : cargo_target := wasm32-wasi
.PHONY: build.examples.rust
build.examples.rust: examples/allocation/rust/testdata/greet.wasm imports/wasi_snapshot_preview1/example/testdata/cargo-wasi/cat.wasm imports/wasi_snapshot_preview1/testdata/cargo-wasi/wasi.wasm internal/testing/dwarftestdata/testdata/rust/main.wasm.xz
# Normally, we build release because it is smaller. Testing dwarf requires the debug build.
internal/testing/dwarftestdata/testdata/rust/main.wasm.xz:
cd $(@D) && cargo wasi build
mv $(@D)/target/wasm32-wasi/debug/main.wasm $(@D)
cd $(@D) && xz -k -f ./main.wasm # Rust's DWARF section is huge, so compress it.
# Builds rust using cargo normally, or cargo-wasi.
%.wasm: %.rs
@(cd $(@D); cargo $(if $(findstring wasi,$(cargo_target)),wasi build,build --target $(cargo_target)) --release)
@mv $(@D)/target/$(cargo_target)/release/$(@F) $(@D)
spectest_base_dir := internal/integration_test/spectest
spectest_v1_dir := $(spectest_base_dir)/v1
spectest_v1_testdata_dir := $(spectest_v1_dir)/testdata
spec_version_v1 := wg-1.0
spectest_v2_dir := $(spectest_base_dir)/v2
spectest_v2_testdata_dir := $(spectest_v2_dir)/testdata
# Latest draft state as of Dec 16, 2022.
spec_version_v2 := 1782235239ddebaf2cb079b00fdaa2d2c4dedba3
.PHONY: build.spectest
build.spectest:
@$(MAKE) build.spectest.v1
@$(MAKE) build.spectest.v2
.PHONY: build.spectest.v1
build.spectest.v1: # Note: wabt by default uses >1.0 features, so wast2json flags might drift as they include more. See WebAssembly/wabt#1878
@rm -rf $(spectest_v1_testdata_dir)
@mkdir -p $(spectest_v1_testdata_dir)
@cd $(spectest_v1_testdata_dir) \
&& curl -sSL 'https://api.github.com/repos/WebAssembly/spec/contents/test/core?ref=$(spec_version_v1)' | jq -r '.[]| .download_url' | grep -E ".wast" | xargs -Iurl curl -sJL url -O
@cd $(spectest_v1_testdata_dir) && for f in `find . -name '*.wast'`; do \
perl -pi -e 's/\(assert_return_canonical_nan\s(\(invoke\s"f32.demote_f64"\s\((f[0-9]{2})\.const\s[a-z0-9.+:-]+\)\))\)/\(assert_return $$1 \(f32.const nan:canonical\)\)/g' $$f; \
perl -pi -e 's/\(assert_return_arithmetic_nan\s(\(invoke\s"f32.demote_f64"\s\((f[0-9]{2})\.const\s[a-z0-9.+:-]+\)\))\)/\(assert_return $$1 \(f32.const nan:arithmetic\)\)/g' $$f; \
perl -pi -e 's/\(assert_return_canonical_nan\s(\(invoke\s"f64\.promote_f32"\s\((f[0-9]{2})\.const\s[a-z0-9.+:-]+\)\))\)/\(assert_return $$1 \(f64.const nan:canonical\)\)/g' $$f; \
perl -pi -e 's/\(assert_return_arithmetic_nan\s(\(invoke\s"f64\.promote_f32"\s\((f[0-9]{2})\.const\s[a-z0-9.+:-]+\)\))\)/\(assert_return $$1 \(f64.const nan:arithmetic\)\)/g' $$f; \
perl -pi -e 's/\(assert_return_canonical_nan\s(\(invoke\s"[a-z._0-9]+"\s\((f[0-9]{2})\.const\s[a-z0-9.+:-]+\)\))\)/\(assert_return $$1 \($$2.const nan:canonical\)\)/g' $$f; \
perl -pi -e 's/\(assert_return_arithmetic_nan\s(\(invoke\s"[a-z._0-9]+"\s\((f[0-9]{2})\.const\s[a-z0-9.+:-]+\)\))\)/\(assert_return $$1 \($$2.const nan:arithmetic\)\)/g' $$f; \
perl -pi -e 's/\(assert_return_canonical_nan\s(\(invoke\s"[a-z._0-9]+"\s\((f[0-9]{2})\.const\s[a-z0-9.+:-]+\)\s\([a-z0-9.\s+-:]+\)\))\)/\(assert_return $$1 \($$2.const nan:canonical\)\)/g' $$f; \
perl -pi -e 's/\(assert_return_arithmetic_nan\s(\(invoke\s"[a-z._0-9]+"\s\((f[0-9]{2})\.const\s[a-z0-9.+:-]+\)\s\([a-z0-9.\s+-:]+\)\))\)/\(assert_return $$1 \($$2.const nan:arithmetic\)\)/g' $$f; \
perl -pi -e 's/\(assert_return_canonical_nan\s(\(invoke\s"[a-z._0-9]+"\s\((f[0-9]{2})\.const\s[a-z0-9.+:-]+\)\))\)/\(assert_return $$1 \($$2.const nan:canonical\)\)/g' $$f; \
perl -pi -e 's/\(assert_return_arithmetic_nan\s(\(invoke\s"[a-z._0-9]+"\s\((f[0-9]{2})\.const\s[a-z0-9.+:-]+\)\))\)/\(assert_return $$1 \($$2.const nan:arithmetic\)\)/g' $$f; \
wast2json \
--disable-saturating-float-to-int \
--disable-sign-extension \
--disable-simd \
--disable-multi-value \
--disable-bulk-memory \
--disable-reference-types \
--debug-names $$f; \
done
.PHONY: build.spectest.v2
build.spectest.v2: # Note: SIMD cases are placed in the "simd" subdirectory.
@mkdir -p $(spectest_v2_testdata_dir)
@cd $(spectest_v2_testdata_dir) \
&& curl -sSL 'https://api.github.com/repos/WebAssembly/spec/contents/test/core?ref=$(spec_version_v2)' | jq -r '.[]| .download_url' | grep -E ".wast" | xargs -Iurl curl -sJL url -O
@cd $(spectest_v2_testdata_dir) \
&& curl -sSL 'https://api.github.com/repos/WebAssembly/spec/contents/test/core/simd?ref=$(spec_version_v2)' | jq -r '.[]| .download_url' | grep -E ".wast" | xargs -Iurl curl -sJL url -O
@cd $(spectest_v2_testdata_dir) && for f in `find . -name '*.wast'`; do \
wast2json --debug-names $$f; \
done
.PHONY: test
test:
@go test $(go_test_options) $$(go list ./... | grep -vE '$(spectest_v1_dir)|$(spectest_v2_dir)')
@cd internal/version/testdata && go test $(go_test_options) ./...
.PHONY: coverage
# replace spaces with commas
coverpkg = $(shell echo $(main_packages) | tr ' ' ',')
coverage: ## Generate test coverage
@go test -coverprofile=coverage.txt -covermode=atomic --coverpkg=$(coverpkg) $(main_packages)
@go tool cover -func coverage.txt
.PHONY: spectest
spectest:
@$(MAKE) spectest.v1
@$(MAKE) spectest.v2
spectest.v1:
@go test $(go_test_options) $$(go list ./... | grep $(spectest_v1_dir))
spectest.v2:
@go test $(go_test_options) $$(go list ./... | grep $(spectest_v2_dir))
golangci_lint_path := $(shell go env GOPATH)/bin/golangci-lint
$(golangci_lint_path):
@go install $(golangci_lint)
golangci_lint_goarch ?= $(shell go env GOARCH)
.PHONY: lint
lint: $(golangci_lint_path)
@GOARCH=$(golangci_lint_goarch) CGO_ENABLED=0 $(golangci_lint_path) run --timeout 5m
.PHONY: format
format:
@go run $(gofumpt) -l -w .
@go run $(gosimports) -local github.com/tetratelabs/ -w $(shell find . -name '*.go' -type f)
@go run $(asmfmt) -w $(shell find . -name '*.s' -type f)
.PHONY: check # Pre-flight check for pull requests
check:
# The following checks help ensure our platform-specific code used for system
# calls safely falls back on a platform unsupported by the compiler engine.
# This makes sure the intepreter can be used. Most often the package that can
# drift here is "platform" or "sysfs":
#
# Ensure we build on windows:
@GOARCH=amd64 GOOS=windows go build ./...
# Ensure we build on an arbitrary operating system:
@GOARCH=amd64 GOOS=dragonfly go build ./...
# Ensure we build on solaris/illumos:
@GOARCH=amd64 GOOS=illumos go build ./...
@GOARCH=amd64 GOOS=solaris go build ./...
# Ensure we build on linux arm for Dapr:
# gh release view -R dapr/dapr --json assets --jq 'first(.assets[] | select(.name = "daprd_linux_arm.tar.gz") | {url, downloadCount})'
@GOARCH=arm GOOS=linux go build ./...
# Ensure we build on linux 386 for Trivy:
# gh release view -R aquasecurity/trivy --json assets --jq 'first(.assets[] | select(.name| test("Linux-32bit.*tar.gz")) | {url, downloadCount})'
@GOARCH=386 GOOS=linux go build ./...
# Ensure we build on FreeBSD amd64 for Trivy:
# gh release view -R aquasecurity/trivy --json assets --jq 'first(.assets[] | select(.name| test("FreeBSD-64bit.*tar.gz")) | {url, downloadCount})'
@GOARCH=amd64 GOOS=freebsd go build ./...
@$(MAKE) lint golangci_lint_goarch=arm64
@$(MAKE) lint golangci_lint_goarch=amd64
@$(MAKE) format
@go mod tidy
@if [ ! -z "`git status -s`" ]; then \
echo "The following differences will fail CI until committed:"; \
git diff --exit-code; \
fi
.PHONY: site
site: ## Serve website content
@git submodule update --init
@cd site && go run $(hugo) server --minify --disableFastRender --baseURL localhost:1313 --cleanDestinationDir -D
.PHONY: clean
clean: ## Ensure a clean build
@rm -rf dist build coverage.txt
@go clean -testcache
fuzz_timeout_seconds ?= 10
.PHONY: fuzz
fuzz:
@cd internal/integration_test/fuzz && cargo fuzz run basic -- -max_total_time=$(fuzz_timeout_seconds)
@cd internal/integration_test/fuzz && cargo fuzz run memory_no_diff -- -max_total_time=$(fuzz_timeout_seconds)
@cd internal/integration_test/fuzz && cargo fuzz run validation -- -max_total_time=$(fuzz_timeout_seconds)
#### CLI release related ####
VERSION ?= dev
# Default to a dummy version 0.0.1.rc1, which is always lower than a real release.
# This must be in the form of [0-255].[0-255].[0-65535] plus optional fourth element which will be ignored.
# We use the fourth field to represent the rc portion of release tag (e.g. rc1 of 1.0.0-rc2).
# https://learn.microsoft.com/en-us/windows/win32/msi/productversion?redirectedfrom=MSDN
# https://stackoverflow.com/questions/9312221/msi-version-numbers
MSI_VERSION ?= 0.0.1.rc1
non_windows_platforms := darwin_amd64 darwin_arm64 linux_amd64 linux_arm64
non_windows_archives := $(non_windows_platforms:%=dist/wazero_$(VERSION)_%.tar.gz)
windows_platforms := windows_amd64 # TODO: add arm64 windows once we start testing on it.
windows_archives := $(windows_platforms:%=dist/wazero_$(VERSION)_%.zip) $(windows_platforms:%=dist/wazero_$(VERSION)_%.msi)
checksum_txt := dist/wazero_$(VERSION)_checksums.txt
# define macros for multi-platform builds. these parse the filename being built
go-arch = $(if $(findstring amd64,$1),amd64,arm64)
go-os = $(if $(findstring .exe,$1),windows,$(if $(findstring linux,$1),linux,darwin))
# msi-arch is a macro so we can detect it based on the file naming convention
msi-arch = $(if $(findstring amd64,$1),x64,arm64)
build/wazero_%/wazero:
$(call go-build,$@,$<)
build/wazero_%/wazero.exe:
$(call go-build,$@,$<)
dist/wazero_$(VERSION)_%.tar.gz: build/wazero_%/wazero
@echo tar.gz "tarring $@"
@mkdir -p $(@D)
@tar -C $(<D) -cpzf $@ $(<F)
@echo tar.gz "ok"
define go-build
@echo "building $1"
@# $(go:go=) removes the trailing 'go', so we can insert cross-build variables
@$(go:go=) CGO_ENABLED=0 GOOS=$(call go-os,$1) GOARCH=$(call go-arch,$1) go build \
-ldflags "-s -w -X github.com/tetratelabs/wazero/internal/version.version=$(VERSION)" \
-o $1 $2 ./cmd/wazero
@echo build "ok"
endef
# this makes a marker file ending in .signed to avoid repeatedly calling codesign
%.signed: %
$(call codesign,$<)
@touch $@
# This requires osslsigncode package (apt or brew) or latest windows release from mtrojnar/osslsigncode
#
# Default is self-signed while production should be a Digicert signing key
#
# Ex.
# ```bash
# keytool -genkey -alias wazero -storetype PKCS12 -keyalg RSA -keysize 2048 -storepass wazero-bunch \
# -keystore wazero.p12 -dname "O=wazero,CN=wazero.io" -validity 3650
# ```
WINDOWS_CODESIGN_P12 ?= packaging/msi/wazero.p12
WINDOWS_CODESIGN_PASSWORD ?= wazero-bunch
define codesign
@printf "$(ansi_format_dark)" codesign "signing $1"
@osslsigncode sign -h sha256 -pkcs12 ${WINDOWS_CODESIGN_P12} -pass "${WINDOWS_CODESIGN_PASSWORD}" \
-n "wazero is the zero dependency WebAssembly runtime for Go developers" -i https://wazero.io -t http://timestamp.digicert.com \
$(if $(findstring msi,$(1)),-add-msi-dse) -in $1 -out $1-signed
@mv $1-signed $1
@printf "$(ansi_format_bright)" codesign "ok"
endef
dist/wazero_$(VERSION)_%.msi: build/wazero_%/wazero.exe.signed
@echo msi "building $@"
@mkdir -p $(@D)
@wixl -a $(call msi-arch,$@) -D Version=$(MSI_VERSION) -D Bin=$(<:.signed=) -o $@ packaging/msi/wazero.wxs
$(call codesign,$@)
@echo msi "ok"
dist/wazero_$(VERSION)_%.zip: build/wazero_%/wazero.exe.signed
@echo zip "zipping $@"
@mkdir -p $(@D)
@zip -qj $@ $(<:.signed=)
@echo zip "ok"
# Darwin doesn't have sha256sum. See https://github.com/actions/virtual-environments/issues/90
sha256sum := $(if $(findstring darwin,$(shell go env GOOS)),shasum -a 256,sha256sum)
$(checksum_txt):
@cd $(@D); touch $(@F); $(sha256sum) * >> $(@F)
dist: $(non_windows_archives) $(windows_archives) $(checksum_txt)
+2
View File
@@ -0,0 +1,2 @@
wazero
Copyright 2020-2021 wazero authors
+1017
View File
File diff suppressed because it is too large. Load diff
+131
View File
@@ -0,0 +1,131 @@
# wazero: the zero dependency WebAssembly runtime for Go developers
[![WebAssembly Core Specification Test](https://github.com/tetratelabs/wazero/actions/workflows/spectest.yaml/badge.svg)](https://github.com/tetratelabs/wazero/actions/workflows/spectest.yaml) [![Go Reference](https://pkg.go.dev/badge/github.com/tetratelabs/wazero.svg)](https://pkg.go.dev/github.com/tetratelabs/wazero) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
WebAssembly is a way to safely run code compiled in other languages. Runtimes
execute WebAssembly Modules (Wasm), which are most often binaries with a `.wasm`
extension.
wazero is a WebAssembly Core Specification [1.0][1] and [2.0][2] compliant
runtime written in Go. It has *zero dependencies*, and doesn't rely on CGO.
This means you can run applications in other languages and still keep cross
compilation.
Import wazero and extend your Go application with code written in any language!
## Example
The best way to learn wazero is by trying one of our [examples](examples/README.md). The
most [basic example](examples/basic) extends a Go application with an addition
function defined in WebAssembly.
## Runtime
There are two runtime configurations supported in wazero: _Compiler_ is default:
By default, ex `wazero.NewRuntime(ctx)`, the Compiler is used if supported. You
can also force the interpreter like so:
```go
r := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfigInterpreter())
```
### Interpreter
Interpreter is a naive interpreter-based implementation of Wasm virtual
machine. Its implementation doesn't have any platform (GOARCH, GOOS) specific
code, therefore _interpreter_ can be used for any compilation target available
for Go (such as `riscv64`).
### Compiler
Compiler compiles WebAssembly modules into machine code ahead of time (AOT),
during `Runtime.CompileModule`. This means your WebAssembly functions execute
natively at runtime. Compiler is faster than Interpreter, often by order of
magnitude (10x) or more. This is done without host-specific dependencies.
If interested, check out the [RATIONALE.md][8] and help us optimize further!
### Conformance
Both runtimes pass WebAssembly Core [1.0][7] and [2.0][14] specification tests
on supported platforms:
| Runtime | Usage | amd64 | arm64 | others |
|:-----------:|:--------------------------------------:|:-----:|:-----:|:------:|
| Interpreter | `wazero.NewRuntimeConfigInterpreter()` | ✅ | ✅ | ✅ |
| Compiler | `wazero.NewRuntimeConfigCompiler()` | ✅ | ✅ | ❌ |
## Support Policy
The below support policy focuses on compatability concerns of those embedding
wazero into their Go applications.
### wazero
wazero is an early project, so APIs are subject to change until version 1.0.
To use wazero meanwhile, you need to use the latest pre-release like this:
```bash
go get github.com/tetratelabs/wazero@latest
```
wazero will tag a new pre-release at least once a month until 1.0. 1.0 is
scheduled for March 2023 and will require minimally Go 1.18. Except
experimental packages, wazero will not break API on subsequent minor versions.
Meanwhile, please practice the current APIs to ensure they work for you, and
give us a [star][15] if you are enjoying it so far!
### Go
wazero has no dependencies except Go, so the only source of conflict in your
project's use of wazero is the Go version.
wazero follows the same version policy as Go's [Release Policy][10]: two
versions. wazero will ensure these versions work and bugs are valid if there's
an issue with a current Go version.
Additionally, wazero intentionally delays usage of language or standard library
features one additional version. For example, when Go 1.29 is released, wazero
can use language features or standard libraries added in 1.27. This is a
convenience for embedders who have a slower version policy than Go. However,
only supported Go versions may be used to raise support issues.
### Platform
wazero has two runtime modes: Interpreter and Compiler. The only supported operating
systems are ones we test, but that doesn't necessarily mean other operating
system versions won't work.
We currently test Linux (Ubuntu and scratch), MacOS and Windows as packaged by
[GitHub Actions][11], as well compilation of 32-bit Linux and 64-bit FreeBSD.
* Interpreter
* Linux is tested on amd64 (native) as well arm64 and riscv64 via emulation.
* MacOS and Windows are only tested on amd64.
* Compiler
* Linux is tested on amd64 (native) as well arm64 via emulation.
* MacOS and Windows are only tested on amd64.
wazero has no dependencies and doesn't require CGO. This means it can also be
embedded in an application that doesn't use an operating system. This is a main
differentiator between wazero and alternatives.
We verify zero dependencies by running tests in Docker's [scratch image][12].
This approach ensures compatibility with any parent image.
-----
wazero is a registered trademark of Tetrate.io, Inc. in the United States and/or other countries
[1]: https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/
[2]: https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/
[4]: https://github.com/WebAssembly/meetings/blob/main/process/subgroups.md
[5]: https://github.com/WebAssembly/WASI
[6]: https://pkg.go.dev/golang.org/x/sys/unix
[7]: https://github.com/WebAssembly/spec/tree/wg-1.0/test/core
[8]: internal/engine/compiler/RATIONALE.md
[9]: https://github.com/tetratelabs/wazero/issues/506
[10]: https://go.dev/doc/devel/release
[11]: https://github.com/actions/virtual-environments
[12]: https://docs.docker.com/develop/develop-images/baseimages/#create-a-simple-parent-image-using-scratch
[13]: https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md
[14]: https://github.com/WebAssembly/spec/tree/d39195773112a22b245ffbe864bab6d1182ccb06/test/core
[15]: https://github.com/tetratelabs/wazero/stargazers
+212
View File
@@ -0,0 +1,212 @@
package api
import (
"fmt"
"strings"
)
// CoreFeatures is a bit flag of WebAssembly Core specification features. See
// https://github.com/WebAssembly/proposals for proposals and their status.
//
// Constants define individual features, such as CoreFeatureMultiValue, or
// groups of "finished" features, assigned to a WebAssembly Core Specification
// version, e.g. CoreFeaturesV1 or CoreFeaturesV2.
//
// Note: Numeric values are not intended to be interpreted except as bit flags.
type CoreFeatures uint64
// CoreFeaturesV1 are features included in the WebAssembly Core Specification
// 1.0. As of late 2022, this is the only version that is a Web Standard (W3C
// Recommendation).
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/
const CoreFeaturesV1 = CoreFeatureMutableGlobal
// CoreFeaturesV2 are features included in the WebAssembly Core Specification
// 2.0 (20220419). As of late 2022, version 2.0 is a W3C working draft, not yet
// a Web Standard (W3C Recommendation).
//
// See https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/appendix/changes.html#release-1-1
const CoreFeaturesV2 = CoreFeaturesV1 |
CoreFeatureBulkMemoryOperations |
CoreFeatureMultiValue |
CoreFeatureNonTrappingFloatToIntConversion |
CoreFeatureReferenceTypes |
CoreFeatureSignExtensionOps |
CoreFeatureSIMD
const (
// CoreFeatureBulkMemoryOperations adds instructions modify ranges of
// memory or table entries ("bulk-memory-operations"). This is included in
// CoreFeaturesV2, but not CoreFeaturesV1.
//
// Here are the notable effects:
// - Adds `memory.fill`, `memory.init`, `memory.copy` and `data.drop`
// instructions.
// - Adds `table.init`, `table.copy` and `elem.drop` instructions.
// - Introduces a "passive" form of element and data segments.
// - Stops checking "active" element and data segment boundaries at
// compile-time, meaning they can error at runtime.
//
// Note: "bulk-memory-operations" is mixed with the "reference-types"
// proposal due to the WebAssembly Working Group merging them
// "mutually dependent". Therefore, enabling this feature requires enabling
// CoreFeatureReferenceTypes, and vice-versa.
//
// See https://github.com/WebAssembly/spec/blob/wg-2.0.draft1/proposals/bulk-memory-operations/Overview.md
// https://github.com/WebAssembly/spec/blob/wg-2.0.draft1/proposals/reference-types/Overview.md and
// https://github.com/WebAssembly/spec/pull/1287
CoreFeatureBulkMemoryOperations CoreFeatures = 1 << iota
// CoreFeatureMultiValue enables multiple values ("multi-value"). This is
// included in CoreFeaturesV2, but not CoreFeaturesV1.
//
// Here are the notable effects:
// - Function (`func`) types allow more than one result.
// - Block types (`block`, `loop` and `if`) can be arbitrary function
// types.
//
// See https://github.com/WebAssembly/spec/blob/wg-2.0.draft1/proposals/multi-value/Overview.md
CoreFeatureMultiValue
// CoreFeatureMutableGlobal allows globals to be mutable. This is included
// in both CoreFeaturesV1 and CoreFeaturesV2.
//
// When false, an api.Global can never be cast to an api.MutableGlobal, and
// any wasm that includes global vars will fail to parse.
CoreFeatureMutableGlobal
// CoreFeatureNonTrappingFloatToIntConversion enables non-trapping
// float-to-int conversions ("nontrapping-float-to-int-conversion"). This
// is included in CoreFeaturesV2, but not CoreFeaturesV1.
//
// The only effect of enabling is allowing the following instructions,
// which return 0 on NaN instead of panicking.
// - `i32.trunc_sat_f32_s`
// - `i32.trunc_sat_f32_u`
// - `i32.trunc_sat_f64_s`
// - `i32.trunc_sat_f64_u`
// - `i64.trunc_sat_f32_s`
// - `i64.trunc_sat_f32_u`
// - `i64.trunc_sat_f64_s`
// - `i64.trunc_sat_f64_u`
//
// See https://github.com/WebAssembly/spec/blob/wg-2.0.draft1/proposals/nontrapping-float-to-int-conversion/Overview.md
CoreFeatureNonTrappingFloatToIntConversion
// CoreFeatureReferenceTypes enables various instructions and features
// related to table and new reference types. This is included in
// CoreFeaturesV2, but not CoreFeaturesV1.
//
// - Introduction of new value types: `funcref` and `externref`.
// - Support for the following new instructions:
// - `ref.null`
// - `ref.func`
// - `ref.is_null`
// - `table.fill`
// - `table.get`
// - `table.grow`
// - `table.set`
// - `table.size`
// - Support for multiple tables per module:
// - `call_indirect`, `table.init`, `table.copy` and `elem.drop`
// - Support for instructions can take non-zero table index.
// - Element segments can take non-zero table index.
//
// Note: "reference-types" is mixed with the "bulk-memory-operations"
// proposal due to the WebAssembly Working Group merging them
// "mutually dependent". Therefore, enabling this feature requires enabling
// CoreFeatureBulkMemoryOperations, and vice-versa.
//
// See https://github.com/WebAssembly/spec/blob/wg-2.0.draft1/proposals/bulk-memory-operations/Overview.md
// https://github.com/WebAssembly/spec/blob/wg-2.0.draft1/proposals/reference-types/Overview.md and
// https://github.com/WebAssembly/spec/pull/1287
CoreFeatureReferenceTypes
// CoreFeatureSignExtensionOps enables sign extension instructions
// ("sign-extension-ops"). This is included in CoreFeaturesV2, but not
// CoreFeaturesV1.
//
// Adds instructions:
// - `i32.extend8_s`
// - `i32.extend16_s`
// - `i64.extend8_s`
// - `i64.extend16_s`
// - `i64.extend32_s`
//
// See https://github.com/WebAssembly/spec/blob/wg-2.0.draft1/proposals/sign-extension-ops/Overview.md
CoreFeatureSignExtensionOps
// CoreFeatureSIMD enables the vector value type and vector instructions
// (aka SIMD). This is included in CoreFeaturesV2, but not CoreFeaturesV1.
//
// Note: The instruction list is too long to enumerate in godoc.
// See https://github.com/WebAssembly/spec/blob/wg-2.0.draft1/proposals/simd/SIMD.md
CoreFeatureSIMD
)
// SetEnabled enables or disables the feature or group of features.
func (f CoreFeatures) SetEnabled(feature CoreFeatures, val bool) CoreFeatures {
if val {
return f | feature
}
return f &^ feature
}
// IsEnabled returns true if the feature (or group of features) is enabled.
func (f CoreFeatures) IsEnabled(feature CoreFeatures) bool {
return f&feature != 0
}
// RequireEnabled returns an error if the feature (or group of features) is not
// enabled.
func (f CoreFeatures) RequireEnabled(feature CoreFeatures) error {
if f&feature == 0 {
return fmt.Errorf("feature %q is disabled", feature)
}
return nil
}
// String implements fmt.Stringer by returning each enabled feature.
func (f CoreFeatures) String() string {
var builder strings.Builder
for i := 0; i <= 63; i++ { // cycle through all bits to reduce code and maintenance
target := CoreFeatures(1 << i)
if f.IsEnabled(target) {
if name := featureName(target); name != "" {
if builder.Len() > 0 {
builder.WriteByte('|')
}
builder.WriteString(name)
}
}
}
return builder.String()
}
func featureName(f CoreFeatures) string {
switch f {
case CoreFeatureMutableGlobal:
// match https://github.com/WebAssembly/mutable-global
return "mutable-global"
case CoreFeatureSignExtensionOps:
// match https://github.com/WebAssembly/spec/blob/wg-2.0.draft1/proposals/sign-extension-ops/Overview.md
return "sign-extension-ops"
case CoreFeatureMultiValue:
// match https://github.com/WebAssembly/spec/blob/wg-2.0.draft1/proposals/multi-value/Overview.md
return "multi-value"
case CoreFeatureNonTrappingFloatToIntConversion:
// match https://github.com/WebAssembly/spec/blob/wg-2.0.draft1/proposals/nontrapping-float-to-int-conversion/Overview.md
return "nontrapping-float-to-int-conversion"
case CoreFeatureBulkMemoryOperations:
// match https://github.com/WebAssembly/spec/blob/wg-2.0.draft1/proposals/bulk-memory-operations/Overview.md
return "bulk-memory-operations"
case CoreFeatureReferenceTypes:
// match https://github.com/WebAssembly/spec/blob/wg-2.0.draft1/proposals/reference-types/Overview.md
return "reference-types"
case CoreFeatureSIMD:
// match https://github.com/WebAssembly/spec/blob/wg-2.0.draft1/proposals/simd/SIMD.md
return "simd"
}
return ""
}
+648
View File
@@ -0,0 +1,648 @@
// Package api includes constants and interfaces used by both end-users and internal implementations.
package api
import (
"context"
"fmt"
"math"
)
// ExternType classifies imports and exports with their respective types.
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#external-types%E2%91%A0
type ExternType = byte
const (
ExternTypeFunc ExternType = 0x00
ExternTypeTable ExternType = 0x01
ExternTypeMemory ExternType = 0x02
ExternTypeGlobal ExternType = 0x03
)
// The below are exported to consolidate parsing behavior for external types.
const (
// ExternTypeFuncName is the name of the WebAssembly 1.0 (20191205) Text Format field for ExternTypeFunc.
ExternTypeFuncName = "func"
// ExternTypeTableName is the name of the WebAssembly 1.0 (20191205) Text Format field for ExternTypeTable.
ExternTypeTableName = "table"
// ExternTypeMemoryName is the name of the WebAssembly 1.0 (20191205) Text Format field for ExternTypeMemory.
ExternTypeMemoryName = "memory"
// ExternTypeGlobalName is the name of the WebAssembly 1.0 (20191205) Text Format field for ExternTypeGlobal.
ExternTypeGlobalName = "global"
)
// ExternTypeName returns the name of the WebAssembly 1.0 (20191205) Text Format field of the given type.
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#exports%E2%91%A4
func ExternTypeName(et ExternType) string {
switch et {
case ExternTypeFunc:
return ExternTypeFuncName
case ExternTypeTable:
return ExternTypeTableName
case ExternTypeMemory:
return ExternTypeMemoryName
case ExternTypeGlobal:
return ExternTypeGlobalName
}
return fmt.Sprintf("%#x", et)
}
// ValueType describes a parameter or result type mapped to a WebAssembly
// function signature.
//
// The following describes how to convert between Wasm and Golang types:
//
// - ValueTypeI32 - EncodeU32 DecodeU32 for uint32 / EncodeI32 DecodeI32 for int32
// - ValueTypeI64 - uint64(int64)
// - ValueTypeF32 - EncodeF32 DecodeF32 from float32
// - ValueTypeF64 - EncodeF64 DecodeF64 from float64
// - ValueTypeExternref - unintptr(unsafe.Pointer(p)) where p is any pointer
// type in Go (e.g. *string)
//
// e.g. Given a Text Format type use (param i64) (result i64), no conversion is
// necessary.
//
// results, _ := fn(ctx, input)
// result := result[0]
//
// e.g. Given a Text Format type use (param f64) (result f64), conversion is
// necessary.
//
// results, _ := fn(ctx, api.EncodeF64(input))
// result := api.DecodeF64(result[0])
//
// Note: This is a type alias as it is easier to encode and decode in the
// binary format.
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-valtype
type ValueType = byte
const (
// ValueTypeI32 is a 32-bit integer.
ValueTypeI32 ValueType = 0x7f
// ValueTypeI64 is a 64-bit integer.
ValueTypeI64 ValueType = 0x7e
// ValueTypeF32 is a 32-bit floating point number.
ValueTypeF32 ValueType = 0x7d
// ValueTypeF64 is a 64-bit floating point number.
ValueTypeF64 ValueType = 0x7c
// ValueTypeExternref is a externref type.
//
// Note: in wazero, externref type value are opaque raw 64-bit pointers,
// and the ValueTypeExternref type in the signature will be translated as
// uintptr in wazero's API level.
//
// For example, given the import function:
// (func (import "env" "f") (param externref) (result externref))
//
// This can be defined in Go as:
// r.NewHostModuleBuilder("env").
// NewFunctionBuilder().
// WithFunc(func(context.Context, _ uintptr) (_ uintptr) { return }).
// Export("f")
//
// Note: The usage of this type is toggled with api.CoreFeatureBulkMemoryOperations.
ValueTypeExternref ValueType = 0x6f
)
// ValueTypeName returns the type name of the given ValueType as a string.
// These type names match the names used in the WebAssembly text format.
//
// Note: This returns "unknown", if an undefined ValueType value is passed.
func ValueTypeName(t ValueType) string {
switch t {
case ValueTypeI32:
return "i32"
case ValueTypeI64:
return "i64"
case ValueTypeF32:
return "f32"
case ValueTypeF64:
return "f64"
case ValueTypeExternref:
return "externref"
}
return "unknown"
}
// Module is a sandboxed, ready to execute Wasm module. This can be used to get exported functions, etc.
//
// In WebAssembly terminology, this corresponds to a "Module Instance", but wazero calls pre-instantiation module as
// "Compiled Module" as in wazero.CompiledModule, therefore we call this post-instantiation module simply "Module".
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#module-instances%E2%91%A0
//
// # Notes
//
// - Closing the wazero.Runtime closes any Module it instantiated.
// - This is an interface for decoupling, not third-party implementations. All implementations are in wazero.
type Module interface {
fmt.Stringer
// Name is the name this module was instantiated with. Exported functions can be imported with this name.
Name() string
// Memory returns a memory defined in this module or nil if there are none wasn't.
Memory() Memory
// ExportedFunction returns a function exported from this module or nil if it wasn't.
ExportedFunction(name string) Function
// ExportedFunctionDefinitions returns all the exported function
// definitions in this module, keyed on export name.
ExportedFunctionDefinitions() map[string]FunctionDefinition
// TODO: Table
// ExportedMemory returns a memory exported from this module or nil if it wasn't.
//
// WASI modules require exporting a Memory named "memory". This means that a module successfully initialized
// as a WASI Command or Reactor will never return nil for this name.
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/design/application-abi.md#current-unstable-abi
ExportedMemory(name string) Memory
// ExportedMemoryDefinitions returns all the exported memory definitions
// in this module, keyed on export name.
//
// Note: As of WebAssembly Core Specification 2.0, there can be at most one
// memory.
ExportedMemoryDefinitions() map[string]MemoryDefinition
// ExportedGlobal a global exported from this module or nil if it wasn't.
ExportedGlobal(name string) Global
// CloseWithExitCode releases resources allocated for this Module. Use a non-zero exitCode parameter to indicate a
// failure to ExportedFunction callers.
//
// The error returned here, if present, is about resource de-allocation (such as I/O errors). Only the last error is
// returned, so a non-nil return means at least one error happened. Regardless of error, this Module will
// be removed, making its name available again.
//
// Calling this inside a host function is safe, and may cause ExportedFunction callers to receive a sys.ExitError
// with the exitCode.
CloseWithExitCode(ctx context.Context, exitCode uint32) error
// Closer closes this module by delegating to CloseWithExitCode with an exit code of zero.
Closer
}
// Closer closes a resource.
type Closer interface {
// Close closes the resource.
//
// Note: The context parameter is used for value lookup, such as for
// logging. A canceled or otherwise done context will not prevent Close
// from succeeding.
Close(context.Context) error
}
// ExportDefinition is a WebAssembly type exported in a module
// (wazero.CompiledModule).
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#exports%E2%91%A0
type ExportDefinition interface {
// ModuleName is the possibly empty name of the module defining this
// export.
//
// Note: This may be different from Module.Name, because a compiled module
// can be instantiated multiple times as different names.
ModuleName() string
// Index is the position in the module's index, imports first.
Index() uint32
// Import returns true with the module and name when this was imported.
// Otherwise, it returns false.
//
// Note: Empty string is valid for both names in the WebAssembly Core
// Specification, so "" "" is possible.
Import() (moduleName, name string, isImport bool)
// ExportNames include all exported names.
//
// Note: The empty name is allowed in the WebAssembly Core Specification,
// so "" is possible.
ExportNames() []string
}
// MemoryDefinition is a WebAssembly memory exported in a module
// (wazero.CompiledModule). Units are in pages (64KB).
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#exports%E2%91%A0
type MemoryDefinition interface {
ExportDefinition
// Min returns the possibly zero initial count of 64KB pages.
Min() uint32
// Max returns the possibly zero max count of 64KB pages, or false if
// unbounded.
Max() (uint32, bool)
}
// FunctionDefinition is a WebAssembly function exported in a module
// (wazero.CompiledModule).
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#exports%E2%91%A0
type FunctionDefinition interface {
ExportDefinition
// Name is the module-defined name of the function, which is not necessarily
// the same as its export name.
Name() string
// DebugName identifies this function based on its Index or Name in the
// module. This is used for errors and stack traces. e.g. "env.abort".
//
// When the function name is empty, a substitute name is generated by
// prefixing '$' to its position in the index. Ex ".$0" is the
// first function (possibly imported) in an unnamed module.
//
// The format is dot-delimited module and function name, but there are no
// restrictions on the module and function name. This means either can be
// empty or include dots. e.g. "x.x.x" could mean module "x" and name "x.x",
// or it could mean module "x.x" and name "x".
//
// Note: This name is stable regardless of import or export. For example,
// if Import returns true, the value is still based on the Name or Index
// and not the imported function name.
DebugName() string
// GoFunction is non-nil when implemented by the embedder instead of a wasm
// binary, e.g. via wazero.HostModuleBuilder
//
// The expected results are nil, GoFunction or GoModuleFunction.
GoFunction() interface{}
// ParamTypes are the possibly empty sequence of value types accepted by a
// function with this signature.
//
// See ValueType documentation for encoding rules.
ParamTypes() []ValueType
// ParamNames are index-correlated with ParamTypes or nil if not available
// for one or more parameters.
ParamNames() []string
// ResultTypes are the results of the function.
//
// When WebAssembly 1.0 (20191205), there can be at most one result.
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#result-types%E2%91%A0
//
// See ValueType documentation for encoding rules.
ResultTypes() []ValueType
// ResultNames are index-correlated with ResultTypes or nil if not
// available for one or more results.
ResultNames() []string
}
// Function is a WebAssembly function exported from an instantiated module
// (wazero.Runtime InstantiateModule).
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-func
type Function interface {
// Definition is metadata about this function from its defining module.
Definition() FunctionDefinition
// Call invokes the function with the given parameters and returns any
// results or an error for any failure looking up or invoking the function.
//
// Encoding is described in Definition, and supplying an incorrect count of
// parameters vs FunctionDefinition.ParamTypes is an error.
//
// If the exporting Module was closed during this call, the error returned
// may be a sys.ExitError. See Module.CloseWithExitCode for details.
//
// Call is not goroutine-safe, therefore it is recommended to create
// another Function if you want to invoke the same function concurrently.
// On the other hand, sequential invocations of Call is allowed.
//
// To safely encode/decode params/results expressed as uint64, users are encouraged to
// use api.EncodeXXX or DecodeXXX functions. See the docs on api.ValueType.
//
// When RuntimeConfig.WithCloseOnContextDone is toggled, the invocation of this Call method is ensured to be closed
// whenever one of the three conditions is met. In the event of close, sys.ExitError will be returned and
// the api.Module from which this api.Function is derived will be made closed. See the documentation of
// WithCloseOnContextDone on wazero.RuntimeConfig for detail. See examples in context_done_example_test.go for
// the end-to-end demonstrations of how these terminations can be performed.
Call(ctx context.Context, params ...uint64) ([]uint64, error)
}
// GoModuleFunction is a Function implemented in Go instead of a wasm binary.
// The Module parameter is the calling module, used to access memory or
// exported functions. See GoModuleFunc for an example.
//
// The stack is includes any parameters encoded according to their ValueType.
// Its length is the max of parameter or result length. When there are results,
// write them in order beginning at index zero. Do not use the stack after the
// function returns.
//
// Here's a typical way to read three parameters and write back one.
//
// // read parameters off the stack in index order
// argv, argvBuf := api.DecodeU32(stack[0]), api.DecodeU32(stack[1])
//
// // write results back to the stack in index order
// stack[0] = api.EncodeU32(ErrnoSuccess)
//
// This function can be non-deterministic or cause side effects. It also
// has special properties not defined in the WebAssembly Core specification.
// Notably, this uses the caller's memory (via Module.Memory). See
// https://www.w3.org/TR/wasm-core-1/#host-functions%E2%91%A0
//
// Most end users will not define functions directly with this, as they will
// use reflection or code generators instead. These approaches are more
// idiomatic as they can map go types to ValueType. This type is exposed for
// those willing to trade usability and safety for performance.
//
// To safely decode/encode values from/to the uint64 stack, users are encouraged to use
// api.EncodeXXX or api.DecodeXXX functions. See the docs on api.ValueType.
type GoModuleFunction interface {
Call(ctx context.Context, mod Module, stack []uint64)
}
// GoModuleFunc is a convenience for defining an inlined function.
//
// For example, the following returns an uint32 value read from parameter zero:
//
// api.GoModuleFunc(func(ctx context.Context, mod api.Module, stack []uint64) {
// offset := api.DecodeU32(stack[0]) // read the parameter from the stack
//
// ret, ok := mod.Memory().ReadUint32Le(offset)
// if !ok {
// panic("out of memory")
// }
//
// stack[0] = api.EncodeU32(ret) // add the result back to the stack.
// })
type GoModuleFunc func(ctx context.Context, mod Module, stack []uint64)
// Call implements GoModuleFunction.Call.
func (f GoModuleFunc) Call(ctx context.Context, mod Module, stack []uint64) {
f(ctx, mod, stack)
}
// GoFunction is an optimized form of GoModuleFunction which doesn't require
// the Module parameter. See GoFunc for an example.
//
// For example, this function does not need to use the importing module's
// memory or exported functions.
type GoFunction interface {
Call(ctx context.Context, stack []uint64)
}
// GoFunc is a convenience for defining an inlined function.
//
// For example, the following returns the sum of two uint32 parameters:
//
// api.GoFunc(func(ctx context.Context, stack []uint64) {
// x, y := api.DecodeU32(stack[0]), api.DecodeU32(stack[1])
// stack[0] = api.EncodeU32(x + y)
// })
type GoFunc func(ctx context.Context, stack []uint64)
// Call implements GoFunction.Call.
func (f GoFunc) Call(ctx context.Context, stack []uint64) {
f(ctx, stack)
}
// Global is a WebAssembly 1.0 (20191205) global exported from an instantiated module (wazero.Runtime InstantiateModule).
//
// For example, if the value is not mutable, you can read it once:
//
// offset := module.ExportedGlobal("memory.offset").Get()
//
// Globals are allowed by specification to be mutable. However, this can be disabled by configuration. When in doubt,
// safe cast to find out if the value can change. Here's an example:
//
// offset := module.ExportedGlobal("memory.offset")
// if _, ok := offset.(api.MutableGlobal); ok {
// // value can change
// } else {
// // value is constant
// }
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#globals%E2%91%A0
type Global interface {
fmt.Stringer
// Type describes the numeric type of the global.
Type() ValueType
// Get returns the last known value of this global.
//
// See Type for how to decode this value to a Go type.
Get() uint64
}
// MutableGlobal is a Global whose value can be updated at runtime (variable).
type MutableGlobal interface {
Global
// Set updates the value of this global.
//
// See Global.Type for how to encode this value from a Go type.
Set(v uint64)
}
// Memory allows restricted access to a module's memory. Notably, this does not allow growing.
//
// # Notes
//
// - This is an interface for decoupling, not third-party implementations. All implementations are in wazero.
// - This includes all value types available in WebAssembly 1.0 (20191205) and all are encoded little-endian.
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#storage%E2%91%A0
type Memory interface {
// Definition is metadata about this memory from its defining module.
Definition() MemoryDefinition
// Size returns the size in bytes available. e.g. If the underlying memory
// has 1 page: 65536
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#-hrefsyntax-instr-memorymathsfmemorysize%E2%91%A0
Size() uint32
// Grow increases memory by the delta in pages (65536 bytes per page).
// The return val is the previous memory size in pages, or false if the
// delta was ignored as it exceeds MemoryDefinition.Max.
//
// # Notes
//
// - This is the same as the "memory.grow" instruction defined in the
// WebAssembly Core Specification, except returns false instead of -1.
// - When this returns true, any shared views via Read must be refreshed.
//
// See MemorySizer Read and https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#grow-mem
Grow(deltaPages uint32) (previousPages uint32, ok bool)
// ReadByte reads a single byte from the underlying buffer at the offset or returns false if out of range.
ReadByte(offset uint32) (byte, bool)
// ReadUint16Le reads a uint16 in little-endian encoding from the underlying buffer at the offset in or returns
// false if out of range.
ReadUint16Le(offset uint32) (uint16, bool)
// ReadUint32Le reads a uint32 in little-endian encoding from the underlying buffer at the offset in or returns
// false if out of range.
ReadUint32Le(offset uint32) (uint32, bool)
// ReadFloat32Le reads a float32 from 32 IEEE 754 little-endian encoded bits in the underlying buffer at the offset
// or returns false if out of range.
// See math.Float32bits
ReadFloat32Le(offset uint32) (float32, bool)
// ReadUint64Le reads a uint64 in little-endian encoding from the underlying buffer at the offset or returns false
// if out of range.
ReadUint64Le(offset uint32) (uint64, bool)
// ReadFloat64Le reads a float64 from 64 IEEE 754 little-endian encoded bits in the underlying buffer at the offset
// or returns false if out of range.
//
// See math.Float64bits
ReadFloat64Le(offset uint32) (float64, bool)
// Read reads byteCount bytes from the underlying buffer at the offset or
// returns false if out of range.
//
// For example, to search for a NUL-terminated string:
// buf, _ = memory.Read(offset, byteCount)
// n := bytes.IndexByte(buf, 0)
// if n < 0 {
// // Not found!
// }
//
// Write-through
//
// This returns a view of the underlying memory, not a copy. This means any
// writes to the slice returned are visible to Wasm, and any updates from
// Wasm are visible reading the returned slice.
//
// For example:
// buf, _ = memory.Read(offset, byteCount)
// buf[1] = 'a' // writes through to memory, meaning Wasm code see 'a'.
//
// If you don't intend-write through, make a copy of the returned slice.
//
// When to refresh Read
//
// The returned slice disconnects on any capacity change. For example,
// `buf = append(buf, 'a')` might result in a slice that is no longer
// shared. The same exists Wasm side. For example, if Wasm changes its
// memory capacity, ex via "memory.grow"), the host slice is no longer
// shared. Those who need a stable view must set Wasm memory min=max, or
// use wazero.RuntimeConfig WithMemoryCapacityPages to ensure max is always
// allocated.
Read(offset, byteCount uint32) ([]byte, bool)
// WriteByte writes a single byte to the underlying buffer at the offset in or returns false if out of range.
WriteByte(offset uint32, v byte) bool
// WriteUint16Le writes the value in little-endian encoding to the underlying buffer at the offset in or returns
// false if out of range.
WriteUint16Le(offset uint32, v uint16) bool
// WriteUint32Le writes the value in little-endian encoding to the underlying buffer at the offset in or returns
// false if out of range.
WriteUint32Le(offset, v uint32) bool
// WriteFloat32Le writes the value in 32 IEEE 754 little-endian encoded bits to the underlying buffer at the offset
// or returns false if out of range.
//
// See math.Float32bits
WriteFloat32Le(offset uint32, v float32) bool
// WriteUint64Le writes the value in little-endian encoding to the underlying buffer at the offset in or returns
// false if out of range.
WriteUint64Le(offset uint32, v uint64) bool
// WriteFloat64Le writes the value in 64 IEEE 754 little-endian encoded bits to the underlying buffer at the offset
// or returns false if out of range.
//
// See math.Float64bits
WriteFloat64Le(offset uint32, v float64) bool
// Write writes the slice to the underlying buffer at the offset or returns false if out of range.
Write(offset uint32, v []byte) bool
// WriteString writes the string to the underlying buffer at the offset or returns false if out of range.
WriteString(offset uint32, v string) bool
}
// CustomSection contains the name and raw data of a custom section.
type CustomSection interface {
// Name is the name of the custom section
Name() string
// Data is the raw data of the custom section
Data() []byte
}
// EncodeExternref encodes the input as a ValueTypeExternref.
//
// See DecodeExternref
func EncodeExternref(input uintptr) uint64 {
return uint64(input)
}
// DecodeExternref decodes the input as a ValueTypeExternref.
//
// See EncodeExternref
func DecodeExternref(input uint64) uintptr {
return uintptr(input)
}
// EncodeI32 encodes the input as a ValueTypeI32.
func EncodeI32(input int32) uint64 {
return uint64(uint32(input))
}
// DecodeI32 decodes the input as a ValueTypeI32.
func DecodeI32(input uint64) int32 {
return int32(input)
}
// EncodeU32 encodes the input as a ValueTypeI32.
func EncodeU32(input uint32) uint64 {
return uint64(input)
}
// DecodeU32 decodes the input as a ValueTypeI32.
func DecodeU32(input uint64) uint32 {
return uint32(input)
}
// EncodeI64 encodes the input as a ValueTypeI64.
func EncodeI64(input int64) uint64 {
return uint64(input)
}
// EncodeF32 encodes the input as a ValueTypeF32.
//
// See DecodeF32
func EncodeF32(input float32) uint64 {
return uint64(math.Float32bits(input))
}
// DecodeF32 decodes the input as a ValueTypeF32.
//
// See EncodeF32
func DecodeF32(input uint64) float32 {
return math.Float32frombits(uint32(input))
}
// EncodeF64 encodes the input as a ValueTypeF64.
//
// See EncodeF32
func EncodeF64(input float64) uint64 {
return math.Float64bits(input)
}
// DecodeF64 decodes the input as a ValueTypeF64.
//
// See EncodeF64
func DecodeF64(input uint64) float64 {
return math.Float64frombits(input)
}
+349
View File
@@ -0,0 +1,349 @@
package wazero
import (
"context"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/wasm"
)
// HostFunctionBuilder defines a host function (in Go), so that a
// WebAssembly binary (e.g. %.wasm file) can import and use it.
//
// Here's an example of an addition function:
//
// hostModuleBuilder.NewFunctionBuilder().
// WithFunc(func(cxt context.Context, x, y uint32) uint32 {
// return x + y
// }).
// Export("add")
//
// # Memory
//
// All host functions act on the importing api.Module, including any memory
// exported in its binary (%.wasm file). If you are reading or writing memory,
// it is sand-boxed Wasm memory defined by the guest.
//
// Below, `m` is the importing module, defined in Wasm. `fn` is a host function
// added via Export. This means that `x` was read from memory defined in Wasm,
// not arbitrary memory in the process.
//
// fn := func(ctx context.Context, m api.Module, offset uint32) uint32 {
// x, _ := m.Memory().ReadUint32Le(ctx, offset)
// return x
// }
type HostFunctionBuilder interface {
// WithGoFunction is an advanced feature for those who need higher
// performance than WithFunc at the cost of more complexity.
//
// Here's an example addition function:
//
// builder.WithGoFunction(api.GoFunc(func(ctx context.Context, params []uint64) []uint64 {
// x, y := uint32(params[0]), uint32(params[1])
// sum := x + y
// return []uint64{sum}
// }, []api.ValueType{api.ValueTypeI32, api.ValueTypeI32}, []api.ValueType{api.ValueTypeI32})
//
// As you can see above, defining in this way implies knowledge of which
// WebAssembly api.ValueType is appropriate for each parameter and result.
//
// See WithGoModuleFunction if you also need to access the calling module.
WithGoFunction(fn api.GoFunction, params, results []api.ValueType) HostFunctionBuilder
// WithGoModuleFunction is an advanced feature for those who need higher
// performance than WithFunc at the cost of more complexity.
//
// Here's an example addition function that loads operands from memory:
//
// builder.WithGoModuleFunction(api.GoModuleFunc(func(ctx context.Context, mod api.Module, params []uint64) []uint64 {
// mem := m.Memory()
// offset := uint32(params[0])
//
// x, _ := mem.ReadUint32Le(ctx, offset)
// y, _ := mem.ReadUint32Le(ctx, offset + 4) // 32 bits == 4 bytes!
// sum := x + y
//
// return []uint64{sum}
// }, []api.ValueType{api.ValueTypeI32, api.ValueTypeI32}, []api.ValueType{api.ValueTypeI32})
//
// As you can see above, defining in this way implies knowledge of which
// WebAssembly api.ValueType is appropriate for each parameter and result.
//
// See WithGoFunction if you don't need access to the calling module.
WithGoModuleFunction(fn api.GoModuleFunction, params, results []api.ValueType) HostFunctionBuilder
// WithFunc uses reflect.Value to map a go `func` to a WebAssembly
// compatible Signature. An input that isn't a `func` will fail to
// instantiate.
//
// Here's an example of an addition function:
//
// builder.WithFunc(func(cxt context.Context, x, y uint32) uint32 {
// return x + y
// })
//
// # Defining a function
//
// Except for the context.Context and optional api.Module, all parameters
// or result types must map to WebAssembly numeric value types. This means
// uint32, int32, uint64, int64, float32 or float64.
//
// api.Module may be specified as the second parameter, usually to access
// memory. This is important because there are only numeric types in Wasm.
// The only way to share other data is via writing memory and sharing
// offsets.
//
// builder.WithFunc(func(ctx context.Context, m api.Module, offset uint32) uint32 {
// mem := m.Memory()
// x, _ := mem.ReadUint32Le(ctx, offset)
// y, _ := mem.ReadUint32Le(ctx, offset + 4) // 32 bits == 4 bytes!
// return x + y
// })
//
// This example propagates context properly when calling other functions
// exported in the api.Module:
//
// builder.WithFunc(func(ctx context.Context, m api.Module, offset, byteCount uint32) uint32 {
// fn = m.ExportedFunction("__read")
// results, err := fn(ctx, offset, byteCount)
// --snip--
WithFunc(interface{}) HostFunctionBuilder
// WithName defines the optional module-local name of this function, e.g.
// "random_get"
//
// Note: This is not required to match the Export name.
WithName(name string) HostFunctionBuilder
// WithParameterNames defines optional parameter names of the function
// signature, e.x. "buf", "buf_len"
//
// Note: When defined, names must be provided for all parameters.
WithParameterNames(names ...string) HostFunctionBuilder
// WithResultNames defines optional result names of the function
// signature, e.x. "errno"
//
// Note: When defined, names must be provided for all results.
WithResultNames(names ...string) HostFunctionBuilder
// Export exports this to the HostModuleBuilder as the given name, e.g.
// "random_get"
Export(name string) HostModuleBuilder
}
// HostModuleBuilder is a way to define host functions (in Go), so that a
// WebAssembly binary (e.g. %.wasm file) can import and use them.
//
// Specifically, this implements the host side of an Application Binary
// Interface (ABI) like WASI or AssemblyScript.
//
// For example, this defines and instantiates a module named "env" with one
// function:
//
// ctx := context.Background()
// r := wazero.NewRuntime(ctx)
// defer r.Close(ctx) // This closes everything this Runtime created.
//
// hello := func() {
// fmt.Fprintln(stdout, "hello!")
// }
// env, _ := r.NewHostModuleBuilder("env").
// NewFunctionBuilder().WithFunc(hello).Export("hello").
// Instantiate(ctx, r)
//
// If the same module may be instantiated multiple times, it is more efficient
// to separate steps. Here's an example:
//
// compiled, _ := r.NewHostModuleBuilder("env").
// NewFunctionBuilder().WithFunc(getRandomString).Export("get_random_string").
// Compile(ctx)
//
// env1, _ := r.InstantiateModule(ctx, compiled, wazero.NewModuleConfig().WithName("env.1"))
// env2, _ := r.InstantiateModule(ctx, compiled, wazero.NewModuleConfig().WithName("env.2"))
//
// See HostFunctionBuilder for valid host function signatures and other details.
//
// # Notes
//
// - HostModuleBuilder is mutable: each method returns the same instance for
// chaining.
// - methods do not return errors, to allow chaining. Any validation errors
// are deferred until Compile.
// - Insertion order is not retained. Anything defined by this builder is
// sorted lexicographically on Compile.
type HostModuleBuilder interface {
// Note: until golang/go#5860, we can't use example tests to embed code in interface godocs.
// NewFunctionBuilder begins the definition of a host function.
NewFunctionBuilder() HostFunctionBuilder
// Compile returns a CompiledModule that can be instantiated by Runtime.
Compile(context.Context) (CompiledModule, error)
// Instantiate is a convenience that calls Compile, then Runtime.InstantiateModule.
// This can fail for reasons documented on Runtime.InstantiateModule.
//
// Here's an example:
//
// ctx := context.Background()
// r := wazero.NewRuntime(ctx)
// defer r.Close(ctx) // This closes everything this Runtime created.
//
// hello := func() {
// fmt.Fprintln(stdout, "hello!")
// }
// env, _ := r.NewHostModuleBuilder("env").
// NewFunctionBuilder().WithFunc(hello).Export("hello").
// Instantiate(ctx)
//
// # Notes
//
// - Closing the Runtime has the same effect as closing the result.
// - Fields in the builder are copied during instantiation: Later changes do not affect the instantiated result.
// - To avoid using configuration defaults, use Compile instead.
Instantiate(context.Context) (api.Module, error)
}
// hostModuleBuilder implements HostModuleBuilder
type hostModuleBuilder struct {
r *runtime
moduleName string
nameToGoFunc map[string]interface{}
funcToNames map[string]*wasm.HostFuncNames
}
// NewHostModuleBuilder implements Runtime.NewHostModuleBuilder
func (r *runtime) NewHostModuleBuilder(moduleName string) HostModuleBuilder {
return &hostModuleBuilder{
r: r,
moduleName: moduleName,
nameToGoFunc: map[string]interface{}{},
funcToNames: map[string]*wasm.HostFuncNames{},
}
}
// hostFunctionBuilder implements HostFunctionBuilder
type hostFunctionBuilder struct {
b *hostModuleBuilder
fn interface{}
name string
paramNames []string
resultNames []string
}
// WithGoFunction implements HostFunctionBuilder.WithGoFunction
func (h *hostFunctionBuilder) WithGoFunction(fn api.GoFunction, params, results []api.ValueType) HostFunctionBuilder {
h.fn = &wasm.HostFunc{
ParamTypes: params,
ResultTypes: results,
Code: wasm.Code{GoFunc: fn},
}
return h
}
// WithGoModuleFunction implements HostFunctionBuilder.WithGoModuleFunction
func (h *hostFunctionBuilder) WithGoModuleFunction(fn api.GoModuleFunction, params, results []api.ValueType) HostFunctionBuilder {
h.fn = &wasm.HostFunc{
ParamTypes: params,
ResultTypes: results,
Code: wasm.Code{GoFunc: fn},
}
return h
}
// WithFunc implements HostFunctionBuilder.WithFunc
func (h *hostFunctionBuilder) WithFunc(fn interface{}) HostFunctionBuilder {
h.fn = fn
return h
}
// WithName implements HostFunctionBuilder.WithName
func (h *hostFunctionBuilder) WithName(name string) HostFunctionBuilder {
h.name = name
return h
}
// WithParameterNames implements HostFunctionBuilder.WithParameterNames
func (h *hostFunctionBuilder) WithParameterNames(names ...string) HostFunctionBuilder {
h.paramNames = names
return h
}
// WithResultNames implements HostFunctionBuilder.WithResultNames
func (h *hostFunctionBuilder) WithResultNames(names ...string) HostFunctionBuilder {
h.resultNames = names
return h
}
// Export implements HostFunctionBuilder.Export
func (h *hostFunctionBuilder) Export(exportName string) HostModuleBuilder {
if h.name == "" {
h.name = exportName
}
names := &wasm.HostFuncNames{
Name: h.name,
ParamNames: h.paramNames,
ResultNames: h.resultNames,
}
if fn, ok := h.fn.(*wasm.HostFunc); ok {
if fn.Name == "" {
fn.Name = names.Name
}
fn.ParamNames = names.ParamNames
fn.ResultNames = names.ResultNames
fn.ExportNames = []string{exportName}
}
h.b.nameToGoFunc[exportName] = h.fn
h.b.funcToNames[exportName] = names
return h.b
}
// ExportHostFunc implements wasm.HostFuncExporter
func (b *hostModuleBuilder) ExportHostFunc(fn *wasm.HostFunc) {
b.nameToGoFunc[fn.ExportNames[0]] = fn
}
// NewFunctionBuilder implements HostModuleBuilder.NewFunctionBuilder
func (b *hostModuleBuilder) NewFunctionBuilder() HostFunctionBuilder {
return &hostFunctionBuilder{b: b}
}
// Compile implements HostModuleBuilder.Compile
func (b *hostModuleBuilder) Compile(ctx context.Context) (CompiledModule, error) {
module, err := wasm.NewHostModule(b.moduleName, b.nameToGoFunc, b.funcToNames, b.r.enabledFeatures)
if err != nil {
return nil, err
} else if err = module.Validate(b.r.enabledFeatures); err != nil {
return nil, err
}
c := &compiledModule{module: module, compiledEngine: b.r.store.Engine}
listeners, err := buildListeners(ctx, module)
if err != nil {
return nil, err
}
if err = b.r.store.Engine.CompileModule(ctx, module, listeners, false); err != nil {
return nil, err
}
// typeIDs are static and compile-time known.
typeIDs, err := b.r.store.GetFunctionTypeIDs(module.TypeSection)
if err != nil {
return nil, err
}
c.typeIDs = typeIDs
return c, nil
}
// Instantiate implements HostModuleBuilder.Instantiate
func (b *hostModuleBuilder) Instantiate(ctx context.Context) (api.Module, error) {
if compiled, err := b.Compile(ctx); err != nil {
return nil, err
} else {
compiled.(*compiledModule).closeWithModule = true
return b.r.InstantiateModule(ctx, compiled, NewModuleConfig())
}
}
+111
View File
@@ -0,0 +1,111 @@
package wazero
import (
"context"
"errors"
"fmt"
"os"
"path"
"path/filepath"
goruntime "runtime"
"sync"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/filecache"
"github.com/tetratelabs/wazero/internal/version"
"github.com/tetratelabs/wazero/internal/wasm"
)
// CompilationCache reduces time spent compiling (Runtime.CompileModule) the same wasm module.
//
// Instances of this can be reused across multiple runtimes, if configured via RuntimeConfig.
type CompilationCache interface{ api.Closer }
// NewCompilationCache returns a new CompilationCache to be passed to RuntimeConfig.
// This configures only in-memory cache, and doesn't persist to the file system. See wazero.NewCompilationCacheWithDir for detail.
//
// The returned CompilationCache can be used to share the in-memory compilation results across multiple instances of wazero.Runtime.
func NewCompilationCache() CompilationCache {
return &cache{}
}
// NewCompilationCacheWithDir is like wazero.NewCompilationCache except the result also writes
// state into the directory specified by `dirname` parameter.
//
// If the dirname doesn't exist, this creates it or returns an error.
//
// Those running wazero as a CLI or frequently restarting a process using the same wasm should
// use this feature to reduce time waiting to compile the same module a second time.
//
// The contents written into dirname are wazero-version specific, meaning different versions of
// wazero will duplicate entries for the same input wasm.
//
// Note: The embedder must safeguard this directory from external changes.
func NewCompilationCacheWithDir(dirname string) (CompilationCache, error) {
c := &cache{}
err := c.ensuresFileCache(dirname, version.GetWazeroVersion())
return c, err
}
// cache implements Cache interface.
type cache struct {
// eng is the engine for this cache. If the cache is configured, the engine is shared across multiple instances of
// Runtime, and its lifetime is not bound to them. Instead, the engine is alive until Cache.Close is called.
engs [engineKindCount]wasm.Engine
fileCache filecache.Cache
initOnces [engineKindCount]sync.Once
}
func (c *cache) initEngine(ek engineKind, ne newEngine, ctx context.Context, features api.CoreFeatures) wasm.Engine {
c.initOnces[ek].Do(func() { c.engs[ek] = ne(ctx, features, c.fileCache) })
return c.engs[ek]
}
// Close implements the same method on the Cache interface.
func (c *cache) Close(_ context.Context) (err error) {
for _, eng := range c.engs {
if eng != nil {
if err = eng.Close(); err != nil {
return
}
}
}
return
}
func (c *cache) ensuresFileCache(dir string, wazeroVersion string) error {
// Resolve a potentially relative directory into an absolute one.
var err error
dir, err = filepath.Abs(dir)
if err != nil {
return err
}
// Ensure the user-supplied directory.
if err = mkdir(dir); err != nil {
return err
}
// Create a version-specific directory to avoid conflicts.
dirname := path.Join(dir, "wazero-"+wazeroVersion+"-"+goruntime.GOARCH+"-"+goruntime.GOOS)
if err = mkdir(dirname); err != nil {
return err
}
c.fileCache = filecache.New(dirname)
return nil
}
func mkdir(dirname string) error {
if st, err := os.Stat(dirname); errors.Is(err, os.ErrNotExist) {
// If the directory not found, create the cache dir.
if err = os.MkdirAll(dirname, 0o700); err != nil {
return fmt.Errorf("create directory %s: %v", dirname, err)
}
} else if err != nil {
return err
} else if !st.IsDir() {
return fmt.Errorf("%s is not dir", dirname)
}
return nil
}
+9
View File
@@ -0,0 +1,9 @@
# Codecov for main is visible here https://app.codecov.io/gh/tetratelabs/wazero
# We use codecov only as a UI, so we disable PR comments and commit status.
# See https://docs.codecov.com/docs/pull-request-comments
comment: false
coverage:
status:
project: off
patch: off
+825
View File
@@ -0,0 +1,825 @@
package wazero
import (
"context"
"errors"
"fmt"
"io"
"io/fs"
"math"
"time"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/engine/compiler"
"github.com/tetratelabs/wazero/internal/engine/interpreter"
"github.com/tetratelabs/wazero/internal/filecache"
"github.com/tetratelabs/wazero/internal/platform"
internalsys "github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/sysfs"
"github.com/tetratelabs/wazero/internal/wasm"
"github.com/tetratelabs/wazero/sys"
)
// RuntimeConfig controls runtime behavior, with the default implementation as
// NewRuntimeConfig
//
// The example below explicitly limits to Wasm Core 1.0 features as opposed to
// relying on defaults:
//
// rConfig = wazero.NewRuntimeConfig().WithCoreFeatures(api.CoreFeaturesV1)
//
// Note: RuntimeConfig is immutable. Each WithXXX function returns a new
// instance including the corresponding change.
type RuntimeConfig interface {
// WithCoreFeatures sets the WebAssembly Core specification features this
// runtime supports. Defaults to api.CoreFeaturesV2.
//
// Example of disabling a specific feature:
// features := api.CoreFeaturesV2.SetEnabled(api.CoreFeatureMutableGlobal, false)
// rConfig = wazero.NewRuntimeConfig().WithCoreFeatures(features)
//
// # Why default to version 2.0?
//
// Many compilers that target WebAssembly require features after
// api.CoreFeaturesV1 by default. For example, TinyGo v0.24+ requires
// api.CoreFeatureBulkMemoryOperations. To avoid runtime errors, wazero
// defaults to api.CoreFeaturesV2, even though it is not yet a Web
// Standard (REC).
WithCoreFeatures(api.CoreFeatures) RuntimeConfig
// WithMemoryLimitPages overrides the maximum pages allowed per memory. The
// default is 65536, allowing 4GB total memory per instance. Setting a
// value larger than default will panic.
//
// This example reduces the largest possible memory size from 4GB to 128KB:
// rConfig = wazero.NewRuntimeConfig().WithMemoryLimitPages(2)
//
// Note: Wasm has 32-bit memory and each page is 65536 (2^16) bytes. This
// implies a max of 65536 (2^16) addressable pages.
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#grow-mem
WithMemoryLimitPages(memoryLimitPages uint32) RuntimeConfig
// WithMemoryCapacityFromMax eagerly allocates max memory, unless max is
// not defined. The default is false, which means minimum memory is
// allocated and any call to grow memory results in re-allocations.
//
// This example ensures any memory.grow instruction will never re-allocate:
// rConfig = wazero.NewRuntimeConfig().WithMemoryCapacityFromMax(true)
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#grow-mem
WithMemoryCapacityFromMax(memoryCapacityFromMax bool) RuntimeConfig
// WithDebugInfoEnabled toggles DWARF based stack traces in the face of
// runtime errors. Defaults to true.
//
// Those who wish to disable this, can like so:
//
// r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfig().WithDebugInfoEnabled(false)
//
// When disabled, a stack trace message looks like:
//
// wasm stack trace:
// .runtime._panic(i32)
// .myFunc()
// .main.main()
// .runtime.run()
// ._start()
//
// When enabled, the stack trace includes source code information:
//
// wasm stack trace:
// .runtime._panic(i32)
// 0x16e2: /opt/homebrew/Cellar/tinygo/0.26.0/src/runtime/runtime_tinygowasm.go:73:6
// .myFunc()
// 0x190b: /Users/XXXXX/wazero/internal/testing/dwarftestdata/testdata/main.go:19:7
// .main.main()
// 0x18ed: /Users/XXXXX/wazero/internal/testing/dwarftestdata/testdata/main.go:4:3
// .runtime.run()
// 0x18cc: /opt/homebrew/Cellar/tinygo/0.26.0/src/runtime/scheduler_none.go:26:10
// ._start()
// 0x18b6: /opt/homebrew/Cellar/tinygo/0.26.0/src/runtime/runtime_wasm_wasi.go:22:5
//
// Note: This only takes into effect when the original Wasm binary has the
// DWARF "custom sections" that are often stripped, depending on
// optimization flags passed to the compiler.
WithDebugInfoEnabled(bool) RuntimeConfig
// WithCompilationCache configures how runtime caches the compiled modules. In the default configuration, compilation results are
// only in-memory until Runtime.Close is closed, and not shareable by multiple Runtime.
//
// Below defines the shared cache across multiple instances of Runtime:
//
// // Creates the new Cache and the runtime configuration with it.
// cache := wazero.NewCompilationCache()
// defer cache.Close()
// config := wazero.NewRuntimeConfig().WithCompilationCache(c)
//
// // Creates two runtimes while sharing compilation caches.
// foo := wazero.NewRuntimeWithConfig(context.Background(), config)
// bar := wazero.NewRuntimeWithConfig(context.Background(), config)
WithCompilationCache(CompilationCache) RuntimeConfig
// WithCustomSections toggles parsing of "custom sections". Defaults to false.
//
// When enabled, it is possible to retrieve custom sections from a CompiledModule:
//
// config := wazero.NewRuntimeConfig().WithCustomSections(true)
// r := wazero.NewRuntimeWithConfig(ctx, config)
// c, err := r.CompileModule(ctx, wasm)
// customSections := c.CustomSections()
WithCustomSections(bool) RuntimeConfig
// WithCloseOnContextDone ensures the executions of functions to be closed under one of the following circumstances:
//
// - context.Context passed to the Call method of api.Function is canceled during execution. (i.e. ctx by context.WithCancel)
// - context.Context passed to the Call method of api.Function reaches timeout during execution. (i.e. ctx by context.WithTimeout or context.WithDeadline)
// - Close or CloseWithExitCode of api.Module is explicitly called during execution.
//
// This is especially useful when one wants to run untrusted Wasm binaries since otherwise, any invocation of
// api.Function can potentially block the corresponding Goroutine forever. Moreover, it might block the
// entire underlying OS thread which runs the api.Function call. See "Why it's safe to execute runtime-generated
// machine codes against async Goroutine preemption" section in internal/engine/compiler/RATIONALE.md for detail.
//
// Note that this comes with a bit of extra cost when enabled. The reason is that internally this forces
// interpreter and compiler runtimes to insert the periodical checks on the conditions above. For that reason,
// this is disabled by default.
//
// See examples in context_done_example_test.go for the end-to-end demonstrations.
//
// When the invocations of api.Function are closed due to this, sys.ExitError is raised to the callers and
// the api.Module from which the functions are derived is made closed.
WithCloseOnContextDone(bool) RuntimeConfig
}
// NewRuntimeConfig returns a RuntimeConfig using the compiler if it is supported in this environment,
// or the interpreter otherwise.
func NewRuntimeConfig() RuntimeConfig {
return newRuntimeConfig()
}
type newEngine func(context.Context, api.CoreFeatures, filecache.Cache) wasm.Engine
type runtimeConfig struct {
enabledFeatures api.CoreFeatures
memoryLimitPages uint32
memoryCapacityFromMax bool
engineKind engineKind
dwarfDisabled bool // negative as defaults to enabled
newEngine newEngine
cache CompilationCache
storeCustomSections bool
ensureTermination bool
}
// engineLessConfig helps avoid copy/pasting the wrong defaults.
var engineLessConfig = &runtimeConfig{
enabledFeatures: api.CoreFeaturesV2,
memoryLimitPages: wasm.MemoryLimitPages,
memoryCapacityFromMax: false,
dwarfDisabled: false,
}
type engineKind int
const (
engineKindCompiler engineKind = iota
engineKindInterpreter
engineKindCount
)
// NewRuntimeConfigCompiler compiles WebAssembly modules into
// runtime.GOARCH-specific assembly for optimal performance.
//
// The default implementation is AOT (Ahead of Time) compilation, applied at
// Runtime.CompileModule. This allows consistent runtime performance, as well
// the ability to reduce any first request penalty.
//
// Note: While this is technically AOT, this does not imply any action on your
// part. wazero automatically performs ahead-of-time compilation as needed when
// Runtime.CompileModule is invoked.
//
// Warning: This panics at runtime if the runtime.GOOS or runtime.GOARCH does not
// support Compiler. Use NewRuntimeConfig to safely detect and fallback to
// NewRuntimeConfigInterpreter if needed.
func NewRuntimeConfigCompiler() RuntimeConfig {
ret := engineLessConfig.clone()
ret.engineKind = engineKindCompiler
ret.newEngine = compiler.NewEngine
return ret
}
// NewRuntimeConfigInterpreter interprets WebAssembly modules instead of compiling them into assembly.
func NewRuntimeConfigInterpreter() RuntimeConfig {
ret := engineLessConfig.clone()
ret.engineKind = engineKindInterpreter
ret.newEngine = interpreter.NewEngine
return ret
}
// clone makes a deep copy of this runtime config.
func (c *runtimeConfig) clone() *runtimeConfig {
ret := *c // copy except maps which share a ref
return &ret
}
// WithCoreFeatures implements RuntimeConfig.WithCoreFeatures
func (c *runtimeConfig) WithCoreFeatures(features api.CoreFeatures) RuntimeConfig {
ret := c.clone()
ret.enabledFeatures = features
return ret
}
// WithCloseOnContextDone implements RuntimeConfig.WithCloseOnContextDone
func (c *runtimeConfig) WithCloseOnContextDone(ensure bool) RuntimeConfig {
ret := c.clone()
ret.ensureTermination = ensure
return ret
}
// WithMemoryLimitPages implements RuntimeConfig.WithMemoryLimitPages
func (c *runtimeConfig) WithMemoryLimitPages(memoryLimitPages uint32) RuntimeConfig {
ret := c.clone()
// This panics instead of returning an error as it is unlikely.
if memoryLimitPages > wasm.MemoryLimitPages {
panic(fmt.Errorf("memoryLimitPages invalid: %d > %d", memoryLimitPages, wasm.MemoryLimitPages))
}
ret.memoryLimitPages = memoryLimitPages
return ret
}
// WithCompilationCache implements RuntimeConfig.WithCompilationCache
func (c *runtimeConfig) WithCompilationCache(ca CompilationCache) RuntimeConfig {
ret := c.clone()
ret.cache = ca
return ret
}
// WithMemoryCapacityFromMax implements RuntimeConfig.WithMemoryCapacityFromMax
func (c *runtimeConfig) WithMemoryCapacityFromMax(memoryCapacityFromMax bool) RuntimeConfig {
ret := c.clone()
ret.memoryCapacityFromMax = memoryCapacityFromMax
return ret
}
// WithDebugInfoEnabled implements RuntimeConfig.WithDebugInfoEnabled
func (c *runtimeConfig) WithDebugInfoEnabled(dwarfEnabled bool) RuntimeConfig {
ret := c.clone()
ret.dwarfDisabled = !dwarfEnabled
return ret
}
// WithCustomSections implements RuntimeConfig.WithCustomSections
func (c *runtimeConfig) WithCustomSections(storeCustomSections bool) RuntimeConfig {
ret := c.clone()
ret.storeCustomSections = storeCustomSections
return ret
}
// CompiledModule is a WebAssembly module ready to be instantiated (Runtime.InstantiateModule) as an api.Module.
//
// In WebAssembly terminology, this is a decoded, validated, and possibly also compiled module. wazero avoids using
// the name "Module" for both before and after instantiation as the name conflation has caused confusion.
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#semantic-phases%E2%91%A0
//
// Note: Closing the wazero.Runtime closes any CompiledModule it compiled.
type CompiledModule interface {
// Name returns the module name encoded into the binary or empty if not.
Name() string
// ImportedFunctions returns all the imported functions
// (api.FunctionDefinition) in this module or nil if there are none.
//
// Note: Unlike ExportedFunctions, there is no unique constraint on
// imports.
ImportedFunctions() []api.FunctionDefinition
// ExportedFunctions returns all the exported functions
// (api.FunctionDefinition) in this module keyed on export name.
ExportedFunctions() map[string]api.FunctionDefinition
// ImportedMemories returns all the imported memories
// (api.MemoryDefinition) in this module or nil if there are none.
//
// ## Notes
// - As of WebAssembly Core Specification 2.0, there can be at most one
// memory.
// - Unlike ExportedMemories, there is no unique constraint on imports.
ImportedMemories() []api.MemoryDefinition
// ExportedMemories returns all the exported memories
// (api.MemoryDefinition) in this module keyed on export name.
//
// Note: As of WebAssembly Core Specification 2.0, there can be at most one
// memory.
ExportedMemories() map[string]api.MemoryDefinition
// CustomSections returns all the custom sections
// (api.CustomSection) in this module keyed on the section name.
CustomSections() []api.CustomSection
// Close releases all the allocated resources for this CompiledModule.
//
// Note: It is safe to call Close while having outstanding calls from an
// api.Module instantiated from this.
Close(context.Context) error
}
// compile-time check to ensure compiledModule implements CompiledModule
var _ CompiledModule = &compiledModule{}
type compiledModule struct {
module *wasm.Module
// compiledEngine holds an engine on which `module` is compiled.
compiledEngine wasm.Engine
// closeWithModule prevents leaking compiled code when a module is compiled implicitly.
closeWithModule bool
typeIDs []wasm.FunctionTypeID
}
// Name implements CompiledModule.Name
func (c *compiledModule) Name() (moduleName string) {
if ns := c.module.NameSection; ns != nil {
moduleName = ns.ModuleName
}
return
}
// Close implements CompiledModule.Close
func (c *compiledModule) Close(context.Context) error {
c.compiledEngine.DeleteCompiledModule(c.module)
// It is possible the underlying may need to return an error later, but in any case this matches api.Module.Close.
return nil
}
// ImportedFunctions implements CompiledModule.ImportedFunctions
func (c *compiledModule) ImportedFunctions() []api.FunctionDefinition {
return c.module.ImportedFunctions()
}
// ExportedFunctions implements CompiledModule.ExportedFunctions
func (c *compiledModule) ExportedFunctions() map[string]api.FunctionDefinition {
return c.module.ExportedFunctions()
}
// ImportedMemories implements CompiledModule.ImportedMemories
func (c *compiledModule) ImportedMemories() []api.MemoryDefinition {
return c.module.ImportedMemories()
}
// ExportedMemories implements CompiledModule.ExportedMemories
func (c *compiledModule) ExportedMemories() map[string]api.MemoryDefinition {
return c.module.ExportedMemories()
}
// CustomSections implements CompiledModule.CustomSections
func (c *compiledModule) CustomSections() []api.CustomSection {
ret := make([]api.CustomSection, len(c.module.CustomSections))
for i, d := range c.module.CustomSections {
ret[i] = &customSection{data: d.Data, name: d.Name}
}
return ret
}
// customSection implements wasm.CustomSection
type customSection struct {
name string
data []byte
}
// Name implements wasm.CustomSection.Name
func (c *customSection) Name() string {
return c.name
}
// Data implements wasm.CustomSection.Data
func (c *customSection) Data() []byte {
return c.data
}
// ModuleConfig configures resources needed by functions that have low-level interactions with the host operating
// system. Using this, resources such as STDIN can be isolated, so that the same module can be safely instantiated
// multiple times.
//
// Here's an example:
//
// // Initialize base configuration:
// config := wazero.NewModuleConfig().WithStdout(buf).WithSysNanotime()
//
// // Assign different configuration on each instantiation
// mod, _ := r.InstantiateModule(ctx, compiled, config.WithName("rotate").WithArgs("rotate", "angle=90", "dir=cw"))
//
// While wazero supports Windows as a platform, host functions using ModuleConfig follow a UNIX dialect.
// See RATIONALE.md for design background and relationship to WebAssembly System Interfaces (WASI).
//
// Note: ModuleConfig is immutable. Each WithXXX function returns a new instance including the corresponding change.
type ModuleConfig interface {
// WithArgs assigns command-line arguments visible to an imported function that reads an arg vector (argv). Defaults to
// none. Runtime.InstantiateModule errs if any arg is empty.
//
// These values are commonly read by the functions like "args_get" in "wasi_snapshot_preview1" although they could be
// read by functions imported from other modules.
//
// Similar to os.Args and exec.Cmd Env, many implementations would expect a program name to be argv[0]. However, neither
// WebAssembly nor WebAssembly System Interfaces (WASI) define this. Regardless, you may choose to set the first
// argument to the same value set via WithName.
//
// Note: This does not default to os.Args as that violates sandboxing.
//
// See https://linux.die.net/man/3/argv and https://en.wikipedia.org/wiki/Null-terminated_string
WithArgs(...string) ModuleConfig
// WithEnv sets an environment variable visible to a Module that imports functions. Defaults to none.
// Runtime.InstantiateModule errs if the key is empty or contains a NULL(0) or equals("") character.
//
// Validation is the same as os.Setenv on Linux and replaces any existing value. Unlike exec.Cmd Env, this does not
// default to the current process environment as that would violate sandboxing. This also does not preserve order.
//
// Environment variables are commonly read by the functions like "environ_get" in "wasi_snapshot_preview1" although
// they could be read by functions imported from other modules.
//
// While similar to process configuration, there are no assumptions that can be made about anything OS-specific. For
// example, neither WebAssembly nor WebAssembly System Interfaces (WASI) define concerns processes have, such as
// case-sensitivity on environment keys. For portability, define entries with case-insensitively unique keys.
//
// See https://linux.die.net/man/3/environ and https://en.wikipedia.org/wiki/Null-terminated_string
WithEnv(key, value string) ModuleConfig
// WithFS is a convenience that calls WithFSConfig with an FSConfig of the
// input for the root ("/") guest path.
WithFS(fs.FS) ModuleConfig
// WithFSConfig configures the filesystem available to each guest
// instantiated with this configuration. By default, no file access is
// allowed, so functions like `path_open` result in unsupported errors
// (e.g. syscall.ENOSYS).
WithFSConfig(FSConfig) ModuleConfig
// WithName configures the module name. Defaults to what was decoded from
// the name section. Empty string ("") clears any name.
WithName(string) ModuleConfig
// WithStartFunctions configures the functions to call after the module is
// instantiated. Defaults to "_start".
//
// # Notes
//
// - If any function doesn't exist, it is skipped. However, all functions
// that do exist are called in order.
// - Some start functions may exit the module during instantiate with a
// sys.ExitError (e.g. emscripten), preventing use of exported functions.
WithStartFunctions(...string) ModuleConfig
// WithStderr configures where standard error (file descriptor 2) is written. Defaults to io.Discard.
//
// This writer is most commonly used by the functions like "fd_write" in "wasi_snapshot_preview1" although it could
// be used by functions imported from other modules.
//
// # Notes
//
// - The caller is responsible to close any io.Writer they supply: It is not closed on api.Module Close.
// - This does not default to os.Stderr as that both violates sandboxing and prevents concurrent modules.
//
// See https://linux.die.net/man/3/stderr
WithStderr(io.Writer) ModuleConfig
// WithStdin configures where standard input (file descriptor 0) is read. Defaults to return io.EOF.
//
// This reader is most commonly used by the functions like "fd_read" in "wasi_snapshot_preview1" although it could
// be used by functions imported from other modules.
//
// # Notes
//
// - The caller is responsible to close any io.Reader they supply: It is not closed on api.Module Close.
// - This does not default to os.Stdin as that both violates sandboxing and prevents concurrent modules.
//
// See https://linux.die.net/man/3/stdin
WithStdin(io.Reader) ModuleConfig
// WithStdout configures where standard output (file descriptor 1) is written. Defaults to io.Discard.
//
// This writer is most commonly used by the functions like "fd_write" in "wasi_snapshot_preview1" although it could
// be used by functions imported from other modules.
//
// # Notes
//
// - The caller is responsible to close any io.Writer they supply: It is not closed on api.Module Close.
// - This does not default to os.Stdout as that both violates sandboxing and prevents concurrent modules.
//
// See https://linux.die.net/man/3/stdout
WithStdout(io.Writer) ModuleConfig
// WithWalltime configures the wall clock, sometimes referred to as the
// real time clock. Defaults to a fake result that increases by 1ms on
// each reading.
//
// Here's an example that uses a custom clock:
// moduleConfig = moduleConfig.
// WithWalltime(func(context.Context) (sec int64, nsec int32) {
// return clock.walltime()
// }, sys.ClockResolution(time.Microsecond.Nanoseconds()))
//
// Note: This does not default to time.Now as that violates sandboxing. Use
// WithSysWalltime for a usable implementation.
WithWalltime(sys.Walltime, sys.ClockResolution) ModuleConfig
// WithSysWalltime uses time.Now for sys.Walltime with a resolution of 1us
// (1000ns).
//
// See WithWalltime
WithSysWalltime() ModuleConfig
// WithNanotime configures the monotonic clock, used to measure elapsed
// time in nanoseconds. Defaults to a fake result that increases by 1ms
// on each reading.
//
// Here's an example that uses a custom clock:
// moduleConfig = moduleConfig.
// WithNanotime(func(context.Context) int64 {
// return clock.nanotime()
// }, sys.ClockResolution(time.Microsecond.Nanoseconds()))
//
// # Notes:
// - This does not default to time.Since as that violates sandboxing.
// - Some compilers implement sleep by looping on sys.Nanotime (e.g. Go).
// - If you set this, you should probably set WithNanosleep also.
// - Use WithSysNanotime for a usable implementation.
WithNanotime(sys.Nanotime, sys.ClockResolution) ModuleConfig
// WithSysNanotime uses time.Now for sys.Nanotime with a resolution of 1us.
//
// See WithNanotime
WithSysNanotime() ModuleConfig
// WithNanosleep configures the how to pause the current goroutine for at
// least the configured nanoseconds. Defaults to return immediately.
//
// This example uses a custom sleep function:
// moduleConfig = moduleConfig.
// WithNanosleep(func(ns int64) {
// rel := unix.NsecToTimespec(ns)
// remain := unix.Timespec{}
// for { // loop until no more time remaining
// err := unix.ClockNanosleep(unix.CLOCK_MONOTONIC, 0, &rel, &remain)
// --snip--
//
// # Notes:
// - This primarily supports `poll_oneoff` for relative clock events.
// - This does not default to time.Sleep as that violates sandboxing.
// - Some compilers implement sleep by looping on sys.Nanotime (e.g. Go).
// - If you set this, you should probably set WithNanotime also.
// - Use WithSysNanosleep for a usable implementation.
WithNanosleep(sys.Nanosleep) ModuleConfig
// WithOsyield yields the processor, typically to implement spin-wait
// loops. Defaults to return immediately.
//
// # Notes:
// - This primarily supports `sched_yield` in WASI
// - This does not default to runtime.osyield as that violates sandboxing.
WithOsyield(sys.Osyield) ModuleConfig
// WithSysNanosleep uses time.Sleep for sys.Nanosleep.
//
// See WithNanosleep
WithSysNanosleep() ModuleConfig
// WithRandSource configures a source of random bytes. Defaults to return a
// deterministic source. You might override this with crypto/rand.Reader
//
// This reader is most commonly used by the functions like "random_get" in
// "wasi_snapshot_preview1", "seed" in AssemblyScript standard "env", and
// "getRandomData" when runtime.GOOS is "js".
//
// Note: The caller is responsible to close any io.Reader they supply: It
// is not closed on api.Module Close.
WithRandSource(io.Reader) ModuleConfig
}
type moduleConfig struct {
name string
nameSet bool
startFunctions []string
stdin io.Reader
stdout io.Writer
stderr io.Writer
randSource io.Reader
walltime *sys.Walltime
walltimeResolution sys.ClockResolution
nanotime *sys.Nanotime
nanotimeResolution sys.ClockResolution
nanosleep *sys.Nanosleep
osyield *sys.Osyield
args [][]byte
// environ is pair-indexed to retain order similar to os.Environ.
environ [][]byte
// environKeys allow overwriting of existing values.
environKeys map[string]int
// fsConfig is the file system configuration for ABI like WASI.
fsConfig FSConfig
}
// NewModuleConfig returns a ModuleConfig that can be used for configuring module instantiation.
func NewModuleConfig() ModuleConfig {
return &moduleConfig{
startFunctions: []string{"_start"},
environKeys: map[string]int{},
}
}
// clone makes a deep copy of this module config.
func (c *moduleConfig) clone() *moduleConfig {
ret := *c // copy except maps which share a ref
ret.environKeys = make(map[string]int, len(c.environKeys))
for key, value := range c.environKeys {
ret.environKeys[key] = value
}
return &ret
}
// WithArgs implements ModuleConfig.WithArgs
func (c *moduleConfig) WithArgs(args ...string) ModuleConfig {
ret := c.clone()
ret.args = toByteSlices(args)
return ret
}
func toByteSlices(strings []string) (result [][]byte) {
if len(strings) == 0 {
return
}
result = make([][]byte, len(strings))
for i, a := range strings {
result[i] = []byte(a)
}
return
}
// WithEnv implements ModuleConfig.WithEnv
func (c *moduleConfig) WithEnv(key, value string) ModuleConfig {
ret := c.clone()
// Check to see if this key already exists and update it.
if i, ok := ret.environKeys[key]; ok {
ret.environ[i+1] = []byte(value) // environ is pair-indexed, so the value is 1 after the key.
} else {
ret.environKeys[key] = len(ret.environ)
ret.environ = append(ret.environ, []byte(key), []byte(value))
}
return ret
}
// WithFS implements ModuleConfig.WithFS
func (c *moduleConfig) WithFS(fs fs.FS) ModuleConfig {
var config FSConfig
if fs != nil {
config = NewFSConfig().WithFSMount(fs, "")
}
return c.WithFSConfig(config)
}
// WithFSConfig implements ModuleConfig.WithFSConfig
func (c *moduleConfig) WithFSConfig(config FSConfig) ModuleConfig {
ret := c.clone()
ret.fsConfig = config
return ret
}
// WithName implements ModuleConfig.WithName
func (c *moduleConfig) WithName(name string) ModuleConfig {
ret := c.clone()
ret.nameSet = true
ret.name = name
return ret
}
// WithStartFunctions implements ModuleConfig.WithStartFunctions
func (c *moduleConfig) WithStartFunctions(startFunctions ...string) ModuleConfig {
ret := c.clone()
ret.startFunctions = startFunctions
return ret
}
// WithStderr implements ModuleConfig.WithStderr
func (c *moduleConfig) WithStderr(stderr io.Writer) ModuleConfig {
ret := c.clone()
ret.stderr = stderr
return ret
}
// WithStdin implements ModuleConfig.WithStdin
func (c *moduleConfig) WithStdin(stdin io.Reader) ModuleConfig {
ret := c.clone()
ret.stdin = stdin
return ret
}
// WithStdout implements ModuleConfig.WithStdout
func (c *moduleConfig) WithStdout(stdout io.Writer) ModuleConfig {
ret := c.clone()
ret.stdout = stdout
return ret
}
// WithWalltime implements ModuleConfig.WithWalltime
func (c *moduleConfig) WithWalltime(walltime sys.Walltime, resolution sys.ClockResolution) ModuleConfig {
ret := c.clone()
ret.walltime = &walltime
ret.walltimeResolution = resolution
return ret
}
// We choose arbitrary resolutions here because there's no perfect alternative. For example, according to the
// source in time.go, windows monotonic resolution can be 15ms. This chooses arbitrarily 1us for wall time and
// 1ns for monotonic. See RATIONALE.md for more context.
// WithSysWalltime implements ModuleConfig.WithSysWalltime
func (c *moduleConfig) WithSysWalltime() ModuleConfig {
return c.WithWalltime(platform.Walltime, sys.ClockResolution(time.Microsecond.Nanoseconds()))
}
// WithNanotime implements ModuleConfig.WithNanotime
func (c *moduleConfig) WithNanotime(nanotime sys.Nanotime, resolution sys.ClockResolution) ModuleConfig {
ret := c.clone()
ret.nanotime = &nanotime
ret.nanotimeResolution = resolution
return ret
}
// WithSysNanotime implements ModuleConfig.WithSysNanotime
func (c *moduleConfig) WithSysNanotime() ModuleConfig {
return c.WithNanotime(platform.Nanotime, sys.ClockResolution(1))
}
// WithNanosleep implements ModuleConfig.WithNanosleep
func (c *moduleConfig) WithNanosleep(nanosleep sys.Nanosleep) ModuleConfig {
ret := *c // copy
ret.nanosleep = &nanosleep
return &ret
}
// WithOsyield implements ModuleConfig.WithOsyield
func (c *moduleConfig) WithOsyield(osyield sys.Osyield) ModuleConfig {
ret := *c // copy
ret.osyield = &osyield
return &ret
}
// WithSysNanosleep implements ModuleConfig.WithSysNanosleep
func (c *moduleConfig) WithSysNanosleep() ModuleConfig {
return c.WithNanosleep(platform.Nanosleep)
}
// WithRandSource implements ModuleConfig.WithRandSource
func (c *moduleConfig) WithRandSource(source io.Reader) ModuleConfig {
ret := c.clone()
ret.randSource = source
return ret
}
// toSysContext creates a baseline wasm.Context configured by ModuleConfig.
func (c *moduleConfig) toSysContext() (sysCtx *internalsys.Context, err error) {
var environ [][]byte // Intentionally doesn't pre-allocate to reduce logic to default to nil.
// Same validation as syscall.Setenv for Linux
for i := 0; i < len(c.environ); i += 2 {
key, value := c.environ[i], c.environ[i+1]
keyLen := len(key)
if keyLen == 0 {
err = errors.New("environ invalid: empty key")
return
}
valueLen := len(value)
result := make([]byte, keyLen+valueLen+1)
j := 0
for ; j < keyLen; j++ {
if k := key[j]; k == '=' { // NUL enforced in NewContext
err = errors.New("environ invalid: key contains '=' character")
return
} else {
result[j] = k
}
}
result[j] = '='
copy(result[j+1:], value)
environ = append(environ, result)
}
var fs sysfs.FS
if f, ok := c.fsConfig.(*fsConfig); ok {
if fs, err = f.toFS(); err != nil {
return
}
}
return internalsys.NewContext(
math.MaxUint32,
c.args,
environ,
c.stdin,
c.stdout,
c.stderr,
c.randSource,
c.walltime, c.walltimeResolution,
c.nanotime, c.nanotimeResolution,
c.nanosleep, c.osyield,
fs,
)
}
+14
View File
@@ -0,0 +1,14 @@
// Note: The build constraints here are about the compiler, which is more
// narrow than the architectures supported by the assembler.
//
// Constraints here must match platform.CompilerSupported.
//
// Meanwhile, users who know their runtime.GOOS can operate with the compiler
// may choose to use NewRuntimeConfigCompiler explicitly.
//go:build (amd64 || arm64) && (darwin || linux || freebsd || windows)
package wazero
func newRuntimeConfig() RuntimeConfig {
return NewRuntimeConfigCompiler()
}
+8
View File
@@ -0,0 +1,8 @@
// This is the opposite constraint of config_supported.go
//go:build !(amd64 || arm64) || !(darwin || linux || freebsd || windows)
package wazero
func newRuntimeConfig() RuntimeConfig {
return NewRuntimeConfigInterpreter()
}
Binary file not shown.
+4
View File
@@ -0,0 +1,4 @@
// Package experimental includes features we aren't yet sure about. These are enabled with context.Context keys.
//
// Note: All features here may be changed or deleted at any time, so use with caution!
package experimental
+65
View File
@@ -0,0 +1,65 @@
package experimental
import (
"context"
"github.com/tetratelabs/wazero/api"
)
// FunctionListenerFactoryKey is a context.Context Value key. Its associated value should be a FunctionListenerFactory.
//
// See https://github.com/tetratelabs/wazero/issues/451
type FunctionListenerFactoryKey struct{}
// FunctionListenerFactory returns FunctionListeners to be notified when a
// function is called.
type FunctionListenerFactory interface {
// NewListener returns a FunctionListener for a defined function. If nil is
// returned, no listener will be notified.
NewListener(api.FunctionDefinition) FunctionListener
// ^^ A single instance can be returned to avoid instantiating a listener
// per function, especially as they may be thousands of functions. Shared
// listeners use their FunctionDefinition parameter to clarify.
}
// FunctionListener can be registered for any function via
// FunctionListenerFactory to be notified when the function is called.
type FunctionListener interface {
// Before is invoked before a function is called. The returned context will
// be used as the context of this function call.
//
// # Params
//
// - ctx: the context of the caller function which must be the same
// instance or parent of the result.
// - mod: the calling module.
// - def: the function definition.
// - paramValues: api.ValueType encoded parameters.
//
// Note: api.Memory is meant for inspection, not modification.
Before(ctx context.Context, mod api.Module, def api.FunctionDefinition, paramValues []uint64) context.Context
// After is invoked after a function is called.
//
// # Params
//
// - ctx: the context returned by Before.
// - mod: the calling module.
// - def: the function definition.
// - err: nil if the function didn't err
// - resultValues: api.ValueType encoded results.
//
// Note: api.Memory is meant for inspection, not modification.
After(ctx context.Context, mod api.Module, def api.FunctionDefinition, err error, resultValues []uint64)
}
// TODO: We need to add tests to enginetest to ensure contexts nest. A good test can use a combination of call and call
// indirect in terms of depth and breadth. The test could show a tree 3 calls deep where the there are a couple calls at
// each depth under the root. The main thing this can help prevent is accidentally swapping the context internally.
// TODO: Errors aren't handled, and the After hook should accept one along with the result values.
// TODO: The context parameter of the After hook is not the same as the Before hook. This means interceptor patterns
// are awkward. e.g. something like timing is difficult as it requires propagating a stack. Otherwise, nested calls will
// overwrite each other's "since" time. Propagating a stack is further awkward as the After hook needs to know the
// position to read from which might be subtle.
Binary file not shown.
+182
View File
@@ -0,0 +1,182 @@
package wazero
import (
"io/fs"
"github.com/tetratelabs/wazero/internal/sysfs"
)
// FSConfig configures filesystem paths the embedding host allows the wasm
// guest to access. Unconfigured paths are not allowed, so functions like
// `path_open` result in unsupported errors (e.g. syscall.ENOSYS).
//
// # Guest Path
//
// `guestPath` is the name of the path the guest should use a filesystem for, or
// empty for any files.
//
// All `guestPath` paths are normalized, specifically removing any leading or
// trailing slashes. This means "/", "./" or "." all coerce to empty "".
//
// Multiple `guestPath` values can be configured, but the last longest match
// wins. For example, if "tmp", then "" were added, a request to open
// "tmp/foo.txt" use the filesystem associated with "tmp" even though a wider
// path, "" (all files), was added later.
//
// A `guestPath` of "." coerces to the empty string "" because the current
// directory is handled by the guest. In other words, the guest resolves ites
// current directory prior to requesting files.
//
// More notes on `guestPath`
// - Go compiled with runtime.GOOS=js do not pay attention to this value.
// Hence, you need to normalize the filesystem with NewRootFS to ensure
// paths requested resolve as expected.
// - Working directories are typically tracked in wasm, though possible some
// relative paths are requested. For example, TinyGo may attempt to resolve
// a path "../.." in unit tests.
// - Zig uses the first path name it sees as the initial working directory of
// the process.
//
// # Scope
//
// Configuration here is module instance scoped. This means you can use the
// same configuration for multiple calls to Runtime.InstantiateModule. Each
// module will have a different file descriptor table. Any errors accessing
// resources allowed here are deferred to instantiation time of each module.
//
// Any host resources present at the time of configuration, but deleted before
// Runtime.InstantiateModule will trap/panic when the guest wasm initializes or
// calls functions like `fd_read`.
//
// # Windows
//
// While wazero supports Windows as a platform, all known compilers use POSIX
// conventions at runtime. For example, even when running on Windows, paths
// used by wasm are separated by forward slash (/), not backslash (\).
//
// # Notes
//
// - FSConfig is immutable. Each WithXXX function returns a new instance
// including the corresponding change.
// - RATIONALE.md includes design background and relationship to WebAssembly
// System Interfaces (WASI).
type FSConfig interface {
// WithDirMount assigns a directory at `dir` to any paths beginning at
// `guestPath`.
//
// For example, `dirPath` as / (or c:\ in Windows), makes the entire host
// volume writeable to the path on the guest. The `guestPath` is always a
// POSIX style path, slash (/) delimited, even if run on Windows.
//
// If the same `guestPath` was assigned before, this overrides its value,
// retaining the original precedence. See the documentation of FSConfig for
// more details on `guestPath`.
//
// # Isolation
//
// The guest will have full access to this directory including escaping it
// via relative path lookups like "../../". Full access includes operations
// such as creating or deleting files, limited to any host level access
// controls.
//
// # os.DirFS
//
// This configuration optimizes for WASI compatability which is sometimes
// at odds with the behavior of os.DirFS. Hence, this will not behave
// exactly the same as os.DirFS. See /RATIONALE.md for more.
WithDirMount(dir, guestPath string) FSConfig
// WithReadOnlyDirMount assigns a directory at `dir` to any paths
// beginning at `guestPath`.
//
// This is the same as WithDirMount except only read operations are
// permitted. However, escaping the directory via relative path lookups
// like "../../" is still allowed.
WithReadOnlyDirMount(dir, guestPath string) FSConfig
// WithFSMount assigns a fs.FS file system for any paths beginning at
// `guestPath`.
//
// If the same `guestPath` was assigned before, this overrides its value,
// retaining the original precedence. See the documentation of FSConfig for
// more details on `guestPath`.
//
// # Isolation
//
// fs.FS does not restrict the ability to overwrite returned files via
// io.Writer. Moreover, os.DirFS documentation includes important notes
// about isolation, which also applies to fs.Sub. As of Go 1.19, the
// built-in file-systems are not jailed (chroot). See
// https://github.com/golang/go/issues/42322
//
// # os.DirFS
//
// Due to limited control and functionality available in os.DirFS, we
// advise using WithDirMount instead. There will be behavior differences
// between os.DirFS and WithDirMount, as the latter biases towards what's
// expected from WASI implementations.
WithFSMount(fs fs.FS, guestPath string) FSConfig
}
type fsConfig struct {
// fs are the currently configured filesystems.
fs []sysfs.FS
// guestPaths are the user-supplied names of the filesystems, retained for
// error messages and fmt.Stringer.
guestPaths []string
// guestPathToFS are the normalized paths to the currently configured
// filesystems, used for de-duplicating.
guestPathToFS map[string]int
}
// NewFSConfig returns a FSConfig that can be used for configuring module instantiation.
func NewFSConfig() FSConfig {
return &fsConfig{guestPathToFS: map[string]int{}}
}
// clone makes a deep copy of this module config.
func (c *fsConfig) clone() *fsConfig {
ret := *c // copy except slice and maps which share a ref
ret.fs = make([]sysfs.FS, 0, len(c.fs))
ret.fs = append(ret.fs, c.fs...)
ret.guestPaths = make([]string, 0, len(c.guestPaths))
ret.guestPaths = append(ret.guestPaths, c.guestPaths...)
ret.guestPathToFS = make(map[string]int, len(c.guestPathToFS))
for key, value := range c.guestPathToFS {
ret.guestPathToFS[key] = value
}
return &ret
}
// WithDirMount implements FSConfig.WithDirMount
func (c *fsConfig) WithDirMount(dir, guestPath string) FSConfig {
return c.withMount(sysfs.NewDirFS(dir), guestPath)
}
// WithReadOnlyDirMount implements FSConfig.WithReadOnlyDirMount
func (c *fsConfig) WithReadOnlyDirMount(dir, guestPath string) FSConfig {
return c.withMount(sysfs.NewReadFS(sysfs.NewDirFS(dir)), guestPath)
}
// WithFSMount implements FSConfig.WithFSMount
func (c *fsConfig) WithFSMount(fs fs.FS, guestPath string) FSConfig {
return c.withMount(sysfs.Adapt(fs), guestPath)
}
func (c *fsConfig) withMount(fs sysfs.FS, guestPath string) FSConfig {
cleaned := sysfs.StripPrefixesAndTrailingSlash(guestPath)
ret := c.clone()
if i, ok := ret.guestPathToFS[cleaned]; ok {
ret.fs[i] = fs
ret.guestPaths[i] = guestPath
} else {
ret.guestPathToFS[cleaned] = len(ret.fs)
ret.fs = append(ret.fs, fs)
ret.guestPaths = append(ret.guestPaths, guestPath)
}
return ret
}
func (c *fsConfig) toFS() (sysfs.FS, error) {
return sysfs.NewRootFS(c.fs, c.guestPaths)
}
+98
View File
@@ -0,0 +1,98 @@
package amd64
import (
"github.com/tetratelabs/wazero/internal/asm"
)
// Assembler is the interface used by amd64 compiler.
type Assembler interface {
asm.AssemblerBase
// CompileJumpToMemory adds jump-type instruction whose destination is stored in the memory address specified by `baseReg+offset`,
// and returns the corresponding Node in the assembled linked list.
CompileJumpToMemory(jmpInstruction asm.Instruction, baseReg asm.Register, offset asm.ConstantValue)
// CompileRegisterToRegisterWithArg adds an instruction where source and destination
// are `from` and `to` registers.
CompileRegisterToRegisterWithArg(instruction asm.Instruction, from, to asm.Register, arg byte)
// CompileMemoryWithIndexToRegister adds an instruction where source operand is the memory address
// specified as `srcBaseReg + srcOffsetConst + srcIndex*srcScale` and destination is the register `dstReg`.
// Note: sourceScale must be one of 1, 2, 4, 8.
CompileMemoryWithIndexToRegister(
instruction asm.Instruction,
srcBaseReg asm.Register,
srcOffsetConst int64,
srcIndex asm.Register,
srcScale int16,
dstReg asm.Register,
)
// CompileMemoryWithIndexAndArgToRegister is the same as CompileMemoryWithIndexToRegister except that this
// also accepts one argument.
CompileMemoryWithIndexAndArgToRegister(
instruction asm.Instruction,
srcBaseReg asm.Register,
srcOffsetConst int64,
srcIndex asm.Register,
srcScale int16,
dstReg asm.Register,
arg byte,
)
// CompileRegisterToMemoryWithIndex adds an instruction where source operand is the register `srcReg`,
// and the destination is the memory address specified as `dstBaseReg + dstOffsetConst + dstIndex*dstScale`
// Note: dstScale must be one of 1, 2, 4, 8.
CompileRegisterToMemoryWithIndex(
instruction asm.Instruction,
srcReg asm.Register,
dstBaseReg asm.Register,
dstOffsetConst int64,
dstIndex asm.Register,
dstScale int16,
)
// CompileRegisterToMemoryWithIndexAndArg is the same as CompileRegisterToMemoryWithIndex except that this
// also accepts one argument.
CompileRegisterToMemoryWithIndexAndArg(
instruction asm.Instruction,
srcReg asm.Register,
dstBaseReg asm.Register,
dstOffsetConst int64,
dstIndex asm.Register,
dstScale int16,
arg byte,
)
// CompileRegisterToConst adds an instruction where source operand is the register `srcRegister`,
// and the destination is the const `value`.
CompileRegisterToConst(instruction asm.Instruction, srcRegister asm.Register, value int64) asm.Node
// CompileRegisterToNone adds an instruction where source operand is the register `register`,
// and there's no destination operand.
CompileRegisterToNone(instruction asm.Instruction, register asm.Register)
// CompileNoneToRegister adds an instruction where destination operand is the register `register`,
// and there's no source operand.
CompileNoneToRegister(instruction asm.Instruction, register asm.Register)
// CompileNoneToMemory adds an instruction where destination operand is the memory address specified
// as `baseReg+offset`. and there's no source operand.
CompileNoneToMemory(instruction asm.Instruction, baseReg asm.Register, offset int64)
// CompileConstToMemory adds an instruction where source operand is the constant `value` and
// the destination is the memory address specified as `dstBaseReg+dstOffset`.
CompileConstToMemory(instruction asm.Instruction, value int64, dstBaseReg asm.Register, dstOffset int64) asm.Node
// CompileMemoryToConst adds an instruction where source operand is the memory address, and
// the destination is the constant `value`.
CompileMemoryToConst(instruction asm.Instruction, srcBaseReg asm.Register, srcOffset int64, value int64) asm.Node
// CompileStaticConstToRegister adds an instruction where the source operand is asm.StaticConst located in the
// memory and the destination is the dstReg.
CompileStaticConstToRegister(instruction asm.Instruction, c *asm.StaticConst, dstReg asm.Register) error
// CompileRegisterToStaticConst adds an instruction where the destination operand is asm.StaticConst located in the
// memory and the source is the srcReg.
CompileRegisterToStaticConst(instruction asm.Instruction, srcReg asm.Register, c *asm.StaticConst) error
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,193 @@
package amd64
import (
"encoding/binary"
"fmt"
"math"
"github.com/tetratelabs/wazero/internal/asm"
)
// defaultMaxDisplacementForConstantPool is the maximum displacement allowed for literal move instructions which access
// the constant pool. This is set as 2 ^30 conservatively while the actual limit is 2^31 since we actually allow this
// limit plus max(length(c) for c in the pool) so we must ensure that limit is less than 2^31.
const defaultMaxDisplacementForConstantPool = 1 << 30
func (a *AssemblerImpl) maybeFlushConstants(isEndOfFunction bool) {
if a.pool.FirstUseOffsetInBinary == nil {
return
}
if isEndOfFunction ||
// If the distance between (the first use in binary) and (end of constant pool) can be larger
// than MaxDisplacementForConstantPool, we have to emit the constant pool now, otherwise
// a const might be unreachable by a literal move whose maximum offset is +- 2^31.
((a.pool.PoolSizeInBytes+a.buf.Len())-int(*a.pool.FirstUseOffsetInBinary)) >= a.MaxDisplacementForConstantPool {
if !isEndOfFunction {
// Adds the jump instruction to skip the constants if this is not the end of function.
//
// TODO: consider NOP padding for this jump, though this rarely happens as most functions should be
// small enough to fit all consts after the end of function.
if a.pool.PoolSizeInBytes >= math.MaxInt8-2 {
// long (near-relative) jump: https://www.felixcloutier.com/x86/jmp
a.buf.WriteByte(0xe9)
a.WriteConst(int64(a.pool.PoolSizeInBytes), 32)
} else {
// short jump: https://www.felixcloutier.com/x86/jmp
a.buf.WriteByte(0xeb)
a.WriteConst(int64(a.pool.PoolSizeInBytes), 8)
}
}
for _, c := range a.pool.Consts {
c.SetOffsetInBinary(uint64(a.buf.Len()))
a.buf.Write(c.Raw)
}
a.pool = asm.NewStaticConstPool() // reset
}
}
type staticConstOpcode struct {
opcode []byte
mandatoryPrefix byte
rex RexPrefix
}
var registerToStaticConstOpcodes = map[asm.Instruction]staticConstOpcode{
// https://www.felixcloutier.com/x86/cmp
CMPL: {opcode: []byte{0x3b}},
CMPQ: {opcode: []byte{0x3b}, rex: RexPrefixW},
}
func (a *AssemblerImpl) encodeRegisterToStaticConst(n *nodeImpl) (err error) {
opc, ok := registerToStaticConstOpcodes[n.instruction]
if !ok {
return errorEncodingUnsupported(n)
}
return a.encodeStaticConstImpl(n, opc.opcode, opc.rex, opc.mandatoryPrefix)
}
var staticConstToRegisterOpcodes = map[asm.Instruction]struct {
opcode []byte
mandatoryPrefix byte
rex RexPrefix
}{
// https://www.felixcloutier.com/x86/movdqu:vmovdqu8:vmovdqu16:vmovdqu32:vmovdqu64
MOVDQU: {mandatoryPrefix: 0xf3, opcode: []byte{0x0f, 0x6f}},
// https://www.felixcloutier.com/x86/lea
LEAQ: {opcode: []byte{0x8d}, rex: RexPrefixW},
// https://www.felixcloutier.com/x86/movupd
MOVUPD: {mandatoryPrefix: 0x66, opcode: []byte{0x0f, 0x10}},
// https://www.felixcloutier.com/x86/mov
MOVL: {opcode: []byte{0x8b}},
MOVQ: {opcode: []byte{0x8b}, rex: RexPrefixW},
// https://www.felixcloutier.com/x86/ucomisd
UCOMISD: {opcode: []byte{0x0f, 0x2e}, mandatoryPrefix: 0x66},
// https://www.felixcloutier.com/x86/ucomiss
UCOMISS: {opcode: []byte{0x0f, 0x2e}},
// https://www.felixcloutier.com/x86/subss
SUBSS: {opcode: []byte{0x0f, 0x5c}, mandatoryPrefix: 0xf3},
// https://www.felixcloutier.com/x86/subsd
SUBSD: {opcode: []byte{0x0f, 0x5c}, mandatoryPrefix: 0xf2},
// https://www.felixcloutier.com/x86/cmp
CMPL: {opcode: []byte{0x39}},
CMPQ: {opcode: []byte{0x39}, rex: RexPrefixW},
// https://www.felixcloutier.com/x86/add
ADDL: {opcode: []byte{0x03}},
ADDQ: {opcode: []byte{0x03}, rex: RexPrefixW},
}
var staticConstToVectorRegisterOpcodes = map[asm.Instruction]staticConstOpcode{
// https://www.felixcloutier.com/x86/mov
MOVL: {opcode: []byte{0x0f, 0x6e}, mandatoryPrefix: 0x66},
MOVQ: {opcode: []byte{0x0f, 0x7e}, mandatoryPrefix: 0xf3},
}
func (a *AssemblerImpl) encodeStaticConstToRegister(n *nodeImpl) (err error) {
var opc staticConstOpcode
var ok bool
if IsVectorRegister(n.dstReg) && (n.instruction == MOVL || n.instruction == MOVQ) {
opc, ok = staticConstToVectorRegisterOpcodes[n.instruction]
} else {
opc, ok = staticConstToRegisterOpcodes[n.instruction]
}
if !ok {
return errorEncodingUnsupported(n)
}
return a.encodeStaticConstImpl(n, opc.opcode, opc.rex, opc.mandatoryPrefix)
}
// encodeStaticConstImpl encodes an instruction where mod:r/m points to the memory location of the static constant n.staticConst,
// and the other operand is the register given at n.srcReg or n.dstReg.
func (a *AssemblerImpl) encodeStaticConstImpl(n *nodeImpl, opcode []byte, rex RexPrefix, mandatoryPrefix byte) (err error) {
a.pool.AddConst(n.staticConst, uint64(a.buf.Len()))
var reg asm.Register
if n.dstReg != asm.NilRegister {
reg = n.dstReg
} else {
reg = n.srcReg
}
reg3Bits, rexPrefix, err := register3bits(reg, registerSpecifierPositionModRMFieldReg)
if err != nil {
return err
}
rexPrefix |= rex
var inst []byte
n.staticConst.AddOffsetFinalizedCallback(func(offsetOfConstInBinary uint64) {
bin := a.buf.Bytes()
displacement := int(offsetOfConstInBinary) - int(n.OffsetInBinary()) - len(inst)
displacementOffsetInInstruction := n.OffsetInBinary() + uint64(len(inst)-4)
binary.LittleEndian.PutUint32(bin[displacementOffsetInInstruction:], uint32(int32(displacement)))
})
// https://wiki.osdev.org/X86-64_Instruction_Encoding#32.2F64-bit_addressing
modRM := 0b00_000_101 | // Indicate "[RIP + 32bit displacement]" encoding.
(reg3Bits << 3) // Place the reg on ModRM:reg.
if mandatoryPrefix != 0 {
inst = append(inst, mandatoryPrefix)
}
if rexPrefix != RexPrefixNone {
inst = append(inst, rexPrefix)
}
inst = append(inst, opcode...)
inst = append(inst, modRM,
0x0, 0x0, 0x0, 0x0, // Preserve 4 bytes for displacement.
)
a.buf.Write(inst)
return
}
// CompileStaticConstToRegister implements Assembler.CompileStaticConstToRegister.
func (a *AssemblerImpl) CompileStaticConstToRegister(instruction asm.Instruction, c *asm.StaticConst, dstReg asm.Register) (err error) {
if len(c.Raw)%2 != 0 {
err = fmt.Errorf("the length of a static constant must be even but was %d", len(c.Raw))
return
}
n := a.newNode(instruction, operandTypesStaticConstToRegister)
n.dstReg = dstReg
n.staticConst = c
return
}
// CompileRegisterToStaticConst implements Assembler.CompileRegisterToStaticConst.
func (a *AssemblerImpl) CompileRegisterToStaticConst(instruction asm.Instruction, srcReg asm.Register, c *asm.StaticConst) (err error) {
if len(c.Raw)%2 != 0 {
err = fmt.Errorf("the length of a static constant must be even but was %d", len(c.Raw))
return
}
n := a.newNode(instruction, operandTypesRegisterToStaticConst)
n.srcReg = srcReg
n.staticConst = c
return
}
+115
View File
@@ -0,0 +1,115 @@
package arm64
import (
"github.com/tetratelabs/wazero/internal/asm"
)
// Assembler is the interface for arm64 specific assembler.
type Assembler interface {
asm.AssemblerBase
// CompileMemoryWithRegisterOffsetToRegister adds an instruction where source operand is the memory address
// specified as `srcBaseReg + srcOffsetReg` and dst is the register `dstReg`.
CompileMemoryWithRegisterOffsetToRegister(instruction asm.Instruction, srcBaseReg, srcOffsetReg, dstReg asm.Register)
// CompileRegisterToMemoryWithRegisterOffset adds an instruction where source operand is the register `srcReg`,
// and the destination is the memory address specified as `dstBaseReg + dstOffsetReg`
CompileRegisterToMemoryWithRegisterOffset(instruction asm.Instruction, srcReg, dstBaseReg, dstOffsetReg asm.Register)
// CompileTwoRegistersToRegister adds an instruction where source operands consists of two registers `src1` and `src2`,
// and the destination is the register `dst`.
CompileTwoRegistersToRegister(instruction asm.Instruction, src1, src2, dst asm.Register)
// CompileThreeRegistersToRegister adds an instruction where source operands consist of three registers
// `src1`, `src2` and `src3`, and destination operands consist of `dst` register.
CompileThreeRegistersToRegister(instruction asm.Instruction, src1, src2, src3, dst asm.Register)
// CompileTwoRegistersToNone adds an instruction where source operands consist of two registers `src1` and `src2`,
// and destination operand is unspecified.
CompileTwoRegistersToNone(instruction asm.Instruction, src1, src2 asm.Register)
// CompileRegisterAndConstToNone adds an instruction where source operands consist of one register `src` and
// constant `srcConst`, and destination operand is unspecified.
CompileRegisterAndConstToNone(instruction asm.Instruction, src asm.Register, srcConst asm.ConstantValue)
// CompileRegisterAndConstToRegister adds an instruction where source operands consist of one register `src` and
// constant `srcConst`, and destination operand is a register `dst`.
CompileRegisterAndConstToRegister(instruction asm.Instruction, src asm.Register, srcConst asm.ConstantValue, dst asm.Register)
// CompileLeftShiftedRegisterToRegister adds an instruction where source operand is the "left shifted register"
// represented as `srcReg << shiftNum` and the destination is the register `dstReg`.
CompileLeftShiftedRegisterToRegister(
instruction asm.Instruction,
shiftedSourceReg asm.Register,
shiftNum asm.ConstantValue,
srcReg, dstReg asm.Register,
)
// CompileConditionalRegisterSet adds an instruction to set 1 on dstReg if the condition satisfies,
// otherwise set 0.
CompileConditionalRegisterSet(cond asm.ConditionalRegisterState, dstReg asm.Register)
// CompileMemoryToVectorRegister adds an instruction where source operands is the memory address specified by
// `srcBaseReg+srcOffset` and the destination is `dstReg` vector register.
CompileMemoryToVectorRegister(instruction asm.Instruction, srcBaseReg asm.Register, srcOffset asm.ConstantValue,
dstReg asm.Register, arrangement VectorArrangement)
// CompileMemoryWithRegisterOffsetToVectorRegister is the same as CompileMemoryToVectorRegister except that the
// offset is specified by the `srcOffsetRegister` register.
CompileMemoryWithRegisterOffsetToVectorRegister(instruction asm.Instruction, srcBaseReg,
srcOffsetRegister asm.Register, dstReg asm.Register, arrangement VectorArrangement)
// CompileVectorRegisterToMemory adds an instruction where source operand is `srcReg` vector register and the
// destination is the memory address specified by `dstBaseReg+dstOffset`.
CompileVectorRegisterToMemory(instruction asm.Instruction, srcReg, dstBaseReg asm.Register,
dstOffset asm.ConstantValue, arrangement VectorArrangement)
// CompileVectorRegisterToMemoryWithRegisterOffset is the same as CompileVectorRegisterToMemory except that the
// offset is specified by the `dstOffsetRegister` register.
CompileVectorRegisterToMemoryWithRegisterOffset(instruction asm.Instruction, srcReg, dstBaseReg,
dstOffsetRegister asm.Register, arrangement VectorArrangement)
// CompileRegisterToVectorRegister adds an instruction where source operand is `srcReg` general purpose register and
// the destination is the `dstReg` vector register. The destination vector's arrangement and index of element can be
// given by `arrangement` and `index`, but not all the instructions will use them.
CompileRegisterToVectorRegister(instruction asm.Instruction, srcReg, dstReg asm.Register,
arrangement VectorArrangement, index VectorIndex)
// CompileVectorRegisterToRegister adds an instruction where destination operand is `dstReg` general purpose register
// and the source is the `srcReg` vector register. The source vector's arrangement and index of element can be
// given by `arrangement` and `index`, but not all the instructions will use them.
CompileVectorRegisterToRegister(instruction asm.Instruction, srcReg, dstReg asm.Register,
arrangement VectorArrangement, index VectorIndex)
// CompileVectorRegisterToVectorRegister adds an instruction where both source and destination operands are vector
// registers. The vector's arrangement can be specified `arrangement`, and the source and destination element's
// index are given by `srcIndex` and `dstIndex` respectively, but not all the instructions will use them.
CompileVectorRegisterToVectorRegister(instruction asm.Instruction, srcReg, dstReg asm.Register,
arrangement VectorArrangement, srcIndex, dstIndex VectorIndex)
// CompileVectorRegisterToVectorRegisterWithConst is the same as CompileVectorRegisterToVectorRegister but the
// additional constant can be provided.
// For example, the const can be used to specify the shift amount for USHLL instruction.
CompileVectorRegisterToVectorRegisterWithConst(instruction asm.Instruction, srcReg, dstReg asm.Register,
arrangement VectorArrangement, c asm.ConstantValue)
// CompileStaticConstToRegister adds an instruction where the source operand is StaticConstant located in
// the memory and the destination is the dstReg.
CompileStaticConstToRegister(instruction asm.Instruction, c *asm.StaticConst, dstReg asm.Register)
// CompileStaticConstToVectorRegister adds an instruction where the source operand is StaticConstant located in
// the memory and the destination is the dstReg.
CompileStaticConstToVectorRegister(instruction asm.Instruction, c *asm.StaticConst, dstReg asm.Register,
arrangement VectorArrangement)
// CompileTwoVectorRegistersToVectorRegister adds an instruction where source are two vectors and destination is one
// vector. The vector's arrangement can be specified `arrangement`.
CompileTwoVectorRegistersToVectorRegister(instruction asm.Instruction, srcReg, srcReg2, dstReg asm.Register,
arrangement VectorArrangement)
// CompileTwoVectorRegistersToVectorRegisterWithConst is the same as CompileTwoVectorRegistersToVectorRegister except
// that this also accept additional constant.
// For example EXIT instruction needs the extraction target immediate as const.
CompileTwoVectorRegistersToVectorRegisterWithConst(instruction asm.Instruction, srcReg, srcReg2, dstReg asm.Register,
arrangement VectorArrangement, c asm.ConstantValue)
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+184
View File
@@ -0,0 +1,184 @@
package asm
import (
"fmt"
"math"
)
// Register represents architecture-specific registers.
type Register byte
// NilRegister is the only architecture-independent register, and
// can be used to indicate that no register is specified.
const NilRegister Register = 0
// Instruction represents architecture-specific instructions.
type Instruction uint16 // to accommodate the high cardinality of vector ops
// ConditionalRegisterState represents architecture-specific conditional
// register's states.
type ConditionalRegisterState byte
// ConditionalRegisterStateUnset is the only architecture-independent conditional state, and
// can be used to indicate that no conditional state is specified.
const ConditionalRegisterStateUnset ConditionalRegisterState = 0
// Node represents a node in the linked list of assembled operations.
type Node interface {
fmt.Stringer
// AssignJumpTarget assigns the given target node as the destination of
// jump instruction for this Node.
AssignJumpTarget(target Node)
// AssignDestinationConstant assigns the given constant as the destination
// of the instruction for this node.
AssignDestinationConstant(value ConstantValue)
// AssignSourceConstant assigns the given constant as the source
// of the instruction for this node.
AssignSourceConstant(value ConstantValue)
// OffsetInBinary returns the offset of this node in the assembled binary.
OffsetInBinary() NodeOffsetInBinary
}
// NodeOffsetInBinary represents an offset of this node in the final binary.
type NodeOffsetInBinary = uint64
// ConstantValue represents a constant value used in an instruction.
type ConstantValue = int64
// StaticConst represents an arbitrary constant bytes which are pooled and emitted by assembler into the binary.
// These constants can be referenced by instructions.
type StaticConst struct {
Raw []byte
// OffsetInBinary is the offset of this static const in the result binary.
OffsetInBinary uint64
// offsetFinalizedCallbacks holds callbacks which are called when .OffsetInBinary is finalized by assembler implementation.
offsetFinalizedCallbacks []func(offsetOfConstInBinary uint64)
}
// NewStaticConst returns the pointer to the new NewStaticConst for given bytes.
func NewStaticConst(raw []byte) *StaticConst {
return &StaticConst{Raw: raw}
}
// AddOffsetFinalizedCallback adds a callback into offsetFinalizedCallbacks.
func (s *StaticConst) AddOffsetFinalizedCallback(cb func(offsetOfConstInBinary uint64)) {
s.offsetFinalizedCallbacks = append(s.offsetFinalizedCallbacks, cb)
}
// SetOffsetInBinary finalizes the offset of this StaticConst, and invokes callbacks.
func (s *StaticConst) SetOffsetInBinary(offset uint64) {
s.OffsetInBinary = offset
for _, cb := range s.offsetFinalizedCallbacks {
cb(offset)
}
}
// StaticConstPool holds a bulk of StaticConst which are yet to be emitted into the binary.
type StaticConstPool struct {
// FirstUseOffsetInBinary holds the offset of the first instruction which accesses this const pool .
FirstUseOffsetInBinary *NodeOffsetInBinary
Consts []*StaticConst
// addedConsts is used to deduplicate the consts to reduce the final size of binary.
// Note: we can use map on .consts field and remove this field,
// but we have the separate field for deduplication in order to have deterministic assembling behavior.
addedConsts map[*StaticConst]struct{}
// PoolSizeInBytes is the current size of the pool in bytes.
PoolSizeInBytes int
}
// NewStaticConstPool returns the pointer to a new StaticConstPool.
func NewStaticConstPool() *StaticConstPool {
return &StaticConstPool{addedConsts: map[*StaticConst]struct{}{}}
}
// AddConst adds a *StaticConst into the pool if it's not already added.
func (p *StaticConstPool) AddConst(c *StaticConst, useOffset NodeOffsetInBinary) {
if _, ok := p.addedConsts[c]; ok {
return
}
if p.FirstUseOffsetInBinary == nil {
p.FirstUseOffsetInBinary = &useOffset
}
p.Consts = append(p.Consts, c)
p.PoolSizeInBytes += len(c.Raw)
p.addedConsts[c] = struct{}{}
}
// AssemblerBase is the common interface for assemblers among multiple architectures.
//
// Note: some of them can be implemented in an arch-independent way, but not all can be
// implemented as such. However, we intentionally put such arch-dependant methods here
// in order to provide the common documentation interface.
type AssemblerBase interface {
// Reset resets the state of Assembler implementation and mark it ready for
// the compilation of the new function compilation.
Reset()
// Assemble produces the final binary for the assembled operations.
Assemble() ([]byte, error)
// SetJumpTargetOnNext instructs the assembler that the next node must be
// assigned to the given node's jump destination.
SetJumpTargetOnNext(nodes ...Node)
// BuildJumpTable calculates the offsets between the first instruction `initialInstructions[0]`
// and others (e.g. initialInstructions[3]), and wrote the calculated offsets into pre-allocated
// `table` StaticConst in little endian.
BuildJumpTable(table *StaticConst, initialInstructions []Node)
// CompileStandAlone adds an instruction to take no arguments.
CompileStandAlone(instruction Instruction) Node
// CompileConstToRegister adds an instruction where source operand is `value` as constant and destination is `destinationReg` register.
CompileConstToRegister(instruction Instruction, value ConstantValue, destinationReg Register) Node
// CompileRegisterToRegister adds an instruction where source and destination operands are registers.
CompileRegisterToRegister(instruction Instruction, from, to Register)
// CompileMemoryToRegister adds an instruction where source operands is the memory address specified by `sourceBaseReg+sourceOffsetConst`
// and the destination is `destinationReg` register.
CompileMemoryToRegister(
instruction Instruction,
sourceBaseReg Register,
sourceOffsetConst ConstantValue,
destinationReg Register,
)
// CompileRegisterToMemory adds an instruction where source operand is `sourceRegister` register and the destination is the
// memory address specified by `destinationBaseRegister+destinationOffsetConst`.
CompileRegisterToMemory(
instruction Instruction,
sourceRegister Register,
destinationBaseRegister Register,
destinationOffsetConst ConstantValue,
)
// CompileJump adds jump-type instruction and returns the corresponding Node in the assembled linked list.
CompileJump(jmpInstruction Instruction) Node
// CompileJumpToRegister adds jump-type instruction whose destination is the memory address specified by `reg` register.
CompileJumpToRegister(jmpInstruction Instruction, reg Register)
// CompileReadInstructionAddress adds an ADR instruction to set the absolute address of "target instruction"
// into destinationRegister. "target instruction" is specified by beforeTargetInst argument and
// the target is determined by "the instruction right after beforeTargetInst type".
//
// For example, if `beforeTargetInst == RET` and we have the instruction sequence like
// `ADR -> X -> Y -> ... -> RET -> MOV`, then the `ADR` instruction emitted by this function set the absolute
// address of `MOV` instruction into the destination register.
CompileReadInstructionAddress(destinationRegister Register, beforeAcquisitionTargetInstruction Instruction)
}
// JumpTableMaximumOffset represents the limit on the size of jump table in bytes.
// When users try loading an extremely large WebAssembly binary which contains a br_table
// statement with approximately 4294967296 (2^32) targets. Realistically speaking, that kind of binary
// could result in more than ten gigabytes of native compiled code where we have to care about
// huge stacks whose height might exceed 32-bit range, and such huge stack doesn't work with the
// current implementation.
const JumpTableMaximumOffset = math.MaxUint32
+59
View File
@@ -0,0 +1,59 @@
package asm
import (
"encoding/binary"
"fmt"
)
// BaseAssemblerImpl includes code common to all architectures.
//
// Note: When possible, add code here instead of in architecture-specific files to reduce drift:
// As this is internal, exporting symbols only to reduce duplication is ok.
type BaseAssemblerImpl struct {
// SetBranchTargetOnNextNodes holds branch kind instructions (BR, conditional BR, etc.)
// where we want to set the next coming instruction as the destination of these BR instructions.
SetBranchTargetOnNextNodes []Node
// JumpTableEntries holds the information to build jump tables.
JumpTableEntries []JumpTableEntry
}
// JumpTableEntry is the necessary data to build a jump table.
// This is exported for testing purpose.
type JumpTableEntry struct {
t *StaticConst
labelInitialInstructions []Node
}
// SetJumpTargetOnNext implements AssemblerBase.SetJumpTargetOnNext
func (a *BaseAssemblerImpl) SetJumpTargetOnNext(nodes ...Node) {
a.SetBranchTargetOnNextNodes = append(a.SetBranchTargetOnNextNodes, nodes...)
}
// BuildJumpTable implements AssemblerBase.BuildJumpTable
func (a *BaseAssemblerImpl) BuildJumpTable(table *StaticConst, labelInitialInstructions []Node) {
a.JumpTableEntries = append(a.JumpTableEntries, JumpTableEntry{
t: table,
labelInitialInstructions: labelInitialInstructions,
})
}
// FinalizeJumpTableEntry finalizes the build tables inside the given code.
func (a *BaseAssemblerImpl) FinalizeJumpTableEntry(code []byte) (err error) {
for i := range a.JumpTableEntries {
ent := &a.JumpTableEntries[i]
labelInitialInstructions := ent.labelInitialInstructions
table := ent.t
// Compile the offset table for each target.
base := labelInitialInstructions[0].OffsetInBinary()
for i, nop := range labelInitialInstructions {
if nop.OffsetInBinary()-base >= JumpTableMaximumOffset {
return fmt.Errorf("too large br_table")
}
// We store the offset from the beginning of the L0's initial instruction.
binary.LittleEndian.PutUint32(code[table.OffsetInBinary+uint64(i*4):table.OffsetInBinary+uint64((i+1)*4)],
uint32(nop.OffsetInBinary())-uint32(base))
}
}
return
}
+145
View File
@@ -0,0 +1,145 @@
package descriptor
import "math/bits"
// Table is a data structure mapping 32 bit descriptor to items.
//
// The data structure optimizes for memory density and lookup performance,
// trading off compute at insertion time. This is a useful compromise for the
// use cases we employ it with: items are usually accessed a lot more often
// than they are inserted, each operation requires a table lookup so we are
// better off spending extra compute to insert items in the table in order to
// get cheaper lookups. Memory efficiency is also crucial to support scaling
// with programs that maintain thousands of items: having a high or non-linear
// memory-to-item ratio could otherwise be used as an attack vector by malicous
// applications attempting to damage performance of the host.
type Table[Key ~uint32, Item any] struct {
masks []uint64
items []Item
}
// Len returns the number of items stored in the table.
func (t *Table[Key, Item]) Len() (n int) {
// We could make this a O(1) operation if we cached the number of items in
// the table. More state usually means more problems, so until we have a
// clear need for this, the simple implementation may be a better trade off.
for _, mask := range t.masks {
n += bits.OnesCount64(mask)
}
return n
}
// Grow ensures that t has enough room for n items, potentially reallocating the
// internal buffers if their capacity was too small to hold this many items.
func (t *Table[Key, Item]) Grow(n int) {
// Round up to a multiple of 64 since this is the smallest increment due to
// using 64 bits masks.
n = (n*64 + 63) / 64
if n > len(t.masks) {
masks := make([]uint64, n)
copy(masks, t.masks)
items := make([]Item, n*64)
copy(items, t.items)
t.masks = masks
t.items = items
}
}
// Insert inserts the given item to the table, returning the key that it is
// mapped to.
//
// The method does not perform deduplication, it is possible for the same item
// to be inserted multiple times, each insertion will return a different key.
func (t *Table[Key, Item]) Insert(item Item) (key Key) {
offset := 0
insert:
// Note: this loop could be made a lot more efficient using vectorized
// operations: 256 bits vector registers would yield a theoretical 4x
// speed up (e.g. using AVX2).
for index, mask := range t.masks[offset:] {
if ^mask != 0 { // not full?
shift := bits.TrailingZeros64(^mask)
index += offset
key = Key(index)*64 + Key(shift)
t.items[key] = item
t.masks[index] = mask | uint64(1<<shift)
return key
}
}
offset = len(t.masks)
n := 2 * len(t.masks)
if n == 0 {
n = 1
}
t.Grow(n)
goto insert
}
// Lookup returns the item associated with the given key (may be nil).
func (t *Table[Key, Item]) Lookup(key Key) (item Item, found bool) {
if i := int(key); i >= 0 && i < len(t.items) {
index := uint(key) / 64
shift := uint(key) % 64
if (t.masks[index] & (1 << shift)) != 0 {
item, found = t.items[i], true
}
}
return
}
// InsertAt inserts the given `item` at the item descriptor `key`.
func (t *Table[Key, Item]) InsertAt(item Item, key Key) {
if diff := int(key) - t.Len(); diff > 0 {
t.Grow(diff)
}
index := uint(key) / 64
shift := uint(key) % 64
t.masks[index] |= 1 << shift
t.items[key] = item
}
// Delete deletes the item stored at the given key from the table.
func (t *Table[Key, Item]) Delete(key Key) {
if index, shift := key/64, key%64; int(index) < len(t.masks) {
mask := t.masks[index]
if (mask & (1 << shift)) != 0 {
var zero Item
t.items[key] = zero
t.masks[index] = mask & ^uint64(1<<shift)
}
}
}
// Range calls f for each item and its associated key in the table. The function
// f might return false to interupt the iteration.
func (t *Table[Key, Item]) Range(f func(Key, Item) bool) {
for i, mask := range t.masks {
if mask == 0 {
continue
}
for j := Key(0); j < 64; j++ {
if (mask & (1 << j)) == 0 {
continue
}
if key := Key(i)*64 + j; !f(key, t.items[key]) {
return
}
}
}
}
// Reset clears the content of the table.
func (t *Table[Key, Item]) Reset() {
for i := range t.masks {
t.masks[i] = 0
}
var zero Item
for i := range t.items {
t.items[i] = zero
}
}
@@ -0,0 +1,77 @@
# Compiler engine
This package implements the Compiler engine for WebAssembly *purely written in Go*.
In this README, we describe the background, technical difficulties and some design choices.
## General limitations on pure Go Compiler engines
In Go program, each Goroutine manages its own stack, and each item on Goroutine
stack is managed by Go runtime for garbage collection, etc.
These impose some difficulties on compiler engine purely written in Go because
we *cannot* use native push/pop instructions to save/restore temporary
variables spilling from registers. This results in making it impossible for us
to invoke Go functions from compiled native codes with the native `call`
instruction since it involves stack manipulations.
*TODO: maybe it is possible to hack the runtime to make it possible to achieve
function calls with `call`.*
## How to generate native codes
wazero uses its own assembler, implemented from scratch in the
[`internal/asm`](../../asm/) package. The primary rationale are wazero's zero
dependency policy, and to enable concurrent compilation (a feature the
WebAssembly binary format optimizes for).
Before this, wazero used [`twitchyliquid64/golang-asm`](https://github.com/twitchyliquid64/golang-asm).
However, this was not only a dependency (one of our goals is to have zero
dependencies), but also a large one (several megabytes added to the binary).
Moreover, any copy of golang-asm is not thread-safe, so can't be used for
concurrent compilation (See [#233](https://github.com/tetratelabs/wazero/issues/233)).
The assembled native codes are represented as `[]byte` and the slice region is
marked as executable via mmap system call.
## How to enter native codes
Assuming that we have a native code as `[]byte`, it is straightforward to enter
the native code region via Go assembly code. In this package, we have the
function without body called `nativecall`
```go
func nativecall(codeSegment, engine, memory uintptr)
```
where we pass `codeSegment uintptr` as a first argument. This pointer is to the
first instruction to be executed. The pointer can be easily derived from
`[]byte` via `unsafe.Pointer`:
```go
code := []byte{}
/* ...Compilation ...*/
codeSegment := uintptr(unsafe.Pointer(&code[0]))
nativecall(codeSegment, ...)
```
And `nativecall` is actually implemented in [arch_amd64.s](./arch_amd64.s)
as a convenience layer to comply with the Go's official calling convention.
We delegate the task to jump into the code segment to the Go assembler code.
## Why it's safe to execute runtime-generated machine codes against async Goroutine preemption
Goroutine preemption is the mechanism of the Go runtime to switch goroutines contexts on an OS thread.
There are two types of preemption: cooperative preemption and async preemption. The former happens, for example,
when making a function call, and it is not an issue for our runtime-generated functions as they do not make
direct function calls to Go-implemented functions. On the other hand, the latter, async preemption, can be problematic
since it tries to interrupt the execution of Goroutine at any point of function, and manipulates CPU register states.
Fortunately, our runtime-generated machine codes do not need to take the async preemption into account.
All the assembly codes are entered via the trampoline implemented as Go Assembler Function (e.g. [arch_amd64.s](./arch_amd64.s)),
and as of Go 1.20, these assembler functions are considered as _unsafe_ for async preemption:
- https://github.com/golang/go/blob/go1.20rc1/src/runtime/preempt.go#L406-L407
- https://github.com/golang/go/blob/9f0234214473dfb785a5ad84a8fc62a6a395cbc3/src/runtime/traceback.go#L227
From the Go runtime point of view, the execution of runtime-generated machine codes is considered as a part of
that trampoline function. Therefore, runtime-generated machine code is also correctly considered unsafe for async preemption.
+17
View File
@@ -0,0 +1,17 @@
package compiler
import "github.com/tetratelabs/wazero/internal/asm"
// newArchContext returns a new archContext which is architecture-specific type to be embedded in callEngine.
// This must be initialized in init() function in architecture-specific arch_*.go file which is guarded by build tag.
var newArchContext func() archContext
// nativecall is used by callEngine.execWasmFunction and the entrypoint to enter the compiled native code.
// codeSegment is the pointer to the initial instruction of the compiled native code.
// ce is "*callEngine" as uintptr.
//
// Note: this is implemented in per-arch Go assembler file. For example, arch_amd64.s implements this for amd64.
func nativecall(codeSegment, ce uintptr, moduleInstanceAddress uintptr)
// registerNameFn is used for debugging purpose to have register symbols in the string of runtimeValueLocation.
var registerNameFn func(register asm.Register) string
@@ -0,0 +1,26 @@
package compiler
import (
"github.com/tetratelabs/wazero/internal/asm/amd64"
)
// init initializes variables for the amd64 architecture
func init() {
newArchContext = newArchContextImpl
registerNameFn = amd64.RegisterName
unreservedGeneralPurposeRegisters = amd64UnreservedGeneralPurposeRegisters
unreservedVectorRegisters = amd64UnreservedVectorRegisters
}
// archContext is embedded in callEngine in order to store architecture-specific data.
// For amd64, this is empty.
type archContext struct{}
// newArchContextImpl implements newArchContext for amd64 architecture.
func newArchContextImpl() (ret archContext) { return }
// newCompiler returns a new compiler interface which can be used to compile the given function instance.
// Note: ir param can be nil for host functions.
func newCompiler() compiler {
return newAmd64Compiler()
}
@@ -0,0 +1,9 @@
#include "funcdata.h"
#include "textflag.h"
// nativecall(codeSegment, ce, moduleInstanceAddress)
TEXT ·nativecall(SB), NOSPLIT|NOFRAME, $0-24
MOVQ ce+8(FP), R13 // Load the address of *callEngine. into amd64ReservedRegisterForCallEngine.
MOVQ moduleInstanceAddress+16(FP), R12 // Load the address of *wasm.ModuleInstance into amd64CallingConventionModuleInstanceAddressRegister.
MOVQ codeSegment+0(FP), AX // Load the address of native code.
JMP AX // Jump to native code.
@@ -0,0 +1,51 @@
package compiler
import (
"math"
"github.com/tetratelabs/wazero/internal/asm/arm64"
)
// init initializes variables for the arm64 architecture
func init() {
newArchContext = newArchContextImpl
registerNameFn = arm64.RegisterName
unreservedGeneralPurposeRegisters = arm64UnreservedGeneralPurposeRegisters
unreservedVectorRegisters = arm64UnreservedVectorRegisters
}
// archContext is embedded in callEngine in order to store architecture-specific data.
type archContext struct {
// compilerCallReturnAddress holds the absolute return address for nativecall.
// The value is set whenever nativecall is executed and done in compiler_arm64.s
// Native code can return to the ce.execWasmFunction's main loop back by
// executing "ret" instruction with this value. See arm64Compiler.exit.
// Note: this is only used by Compiler code so mark this as nolint.
compilerCallReturnAddress uint64 //nolint
// Loading large constants in arm64 is a bit costly, so we place the following
// consts on callEngine struct so that we can quickly access them during various operations.
// minimum32BitSignedInt is used for overflow check for 32-bit signed division.
// Note: this can be obtained by moving $1 and doing left-shift with 31, but it is
// slower than directly loading from this location.
minimum32BitSignedInt int32
// Note: this can be obtained by moving $1 and doing left-shift with 63, but it is
// slower than directly loading from this location.
// minimum64BitSignedInt is used for overflow check for 64-bit signed division.
minimum64BitSignedInt int64
}
// newArchContextImpl implements newArchContext for amd64 architecture.
func newArchContextImpl() archContext {
return archContext{
minimum32BitSignedInt: math.MinInt32,
minimum64BitSignedInt: math.MinInt64,
}
}
// newCompiler returns a new compiler interface which can be used to compile the given function instance.
// Note: ir param can be nil for host functions.
func newCompiler() compiler {
return newArm64Compiler()
}
@@ -0,0 +1,21 @@
#include "funcdata.h"
#include "textflag.h"
// nativecall(codeSegment, ce, moduleInstanceAddress)
TEXT ·nativecall(SB), NOSPLIT|NOFRAME, $0-24
// Load the address of *callEngine into arm64ReservedRegisterForCallEngine.
MOVD ce+8(FP), R0
// In arm64, return address is stored in R30 after jumping into the code.
// We save the return address value into archContext.compilerReturnAddress in Engine.
// Note that the const 144 drifts after editting Engine or archContext struct. See TestArchContextOffsetInEngine.
MOVD R30, 144(R0)
// Load the address of *wasm.ModuleInstance into arm64CallingConventionModuleInstanceAddressRegister.
MOVD moduleInstanceAddress+16(FP), R29
// Load the address of native code.
MOVD codeSegment+0(FP), R1
// Jump to native code.
JMP (R1)
@@ -0,0 +1,16 @@
//go:build !amd64 && !arm64
package compiler
import (
"fmt"
"runtime"
)
// archContext is empty on an unsupported architecture.
type archContext struct{}
// newCompiler panics with an unsupported error.
func newCompiler() compiler {
panic(fmt.Sprintf("unsupported GOARCH %s", runtime.GOARCH))
}
@@ -0,0 +1,3 @@
//go:build !arm64 && !amd64
TEXT ·nativecall(SB), $0-16
@@ -0,0 +1,334 @@
package compiler
import (
"github.com/tetratelabs/wazero/internal/asm"
"github.com/tetratelabs/wazero/internal/wazeroir"
)
// compiler is the interface of architecture-specific native code compiler,
// and this is responsible for compiling native code for all wazeroir operations.
type compiler interface {
Init(ir *wazeroir.CompilationResult, withListener bool)
// String is for debugging purpose.
String() string
// compilePreamble is called before compiling any wazeroir operation.
// This is used, for example, to initialize the reserved registers, etc.
compilePreamble() error
// compile generates the byte slice of native code.
// stackPointerCeil is the max stack pointer that the target function would reach.
compile() (code []byte, stackPointerCeil uint64, err error)
// compileGoHostFunction adds the trampoline code from which native code can jump into the Go-defined host function.
// TODO: maybe we wouldn't need to have trampoline for host functions.
compileGoDefinedHostFunction() error
// compileLabel notify compilers of the beginning of a label.
// Return true if the compiler decided to skip the entire label.
// See wazeroir.OperationLabel
compileLabel(o wazeroir.OperationLabel) (skipThisLabel bool)
// compileUnreachable adds instruction to perform wazeroir.OperationUnreachable.
compileUnreachable() error
// compileSet adds instruction to perform wazeroir.OperationSet.
compileSet(o wazeroir.OperationSet) error
// compileGlobalGet adds instructions to perform wazeroir.OperationGlobalGet.
compileGlobalGet(o wazeroir.OperationGlobalGet) error
// compileGlobalSet adds instructions to perform wazeroir.OperationGlobalSet.
compileGlobalSet(o wazeroir.OperationGlobalSet) error
// compileBr adds instructions to perform wazeroir.OperationBr.
compileBr(o wazeroir.OperationBr) error
// compileBrIf adds instructions to perform wazeroir.OperationBrIf.
compileBrIf(o wazeroir.OperationBrIf) error
// compileBrTable adds instructions to perform wazeroir.OperationBrTable.
compileBrTable(o wazeroir.OperationBrTable) error
// compileCall adds instructions to perform wazeroir.OperationCall.
compileCall(o wazeroir.OperationCall) error
// compileCallIndirect adds instructions to perform wazeroir.OperationCallIndirect.
compileCallIndirect(o wazeroir.OperationCallIndirect) error
// compileDrop adds instructions to perform wazeroir.OperationDrop.
compileDrop(o wazeroir.OperationDrop) error
// compileSelect adds instructions to perform wazeroir.OperationSelect.
compileSelect(o wazeroir.OperationSelect) error
// compilePick adds instructions to perform wazeroir.OperationPick.
compilePick(o wazeroir.OperationPick) error
// compileAdd adds instructions to perform wazeroir.OperationAdd.
compileAdd(o wazeroir.OperationAdd) error
// compileSub adds instructions to perform wazeroir.OperationSub.
compileSub(o wazeroir.OperationSub) error
// compileMul adds instructions to perform wazeroir.OperationMul.
compileMul(o wazeroir.OperationMul) error
// compileClz adds instructions to perform wazeroir.OperationClz.
compileClz(o wazeroir.OperationClz) error
// compileCtz adds instructions to perform wazeroir.OperationCtz.
compileCtz(o wazeroir.OperationCtz) error
// compilePopcnt adds instructions to perform wazeroir.OperationPopcnt.
compilePopcnt(o wazeroir.OperationPopcnt) error
// compileDiv adds instructions to perform wazeroir.OperationDiv.
compileDiv(o wazeroir.OperationDiv) error
// compileRem adds instructions to perform wazeroir.OperationRem.
compileRem(o wazeroir.OperationRem) error
// compileAnd adds instructions to perform wazeroir.OperationAnd.
compileAnd(o wazeroir.OperationAnd) error
// compileOr adds instructions to perform wazeroir.OperationOr.
compileOr(o wazeroir.OperationOr) error
// compileXor adds instructions to perform wazeroir.OperationXor.
compileXor(o wazeroir.OperationXor) error
// compileShl adds instructions to perform wazeroir.OperationShl.
compileShl(o wazeroir.OperationShl) error
// compileShr adds instructions to perform wazeroir.OperationShr.
compileShr(o wazeroir.OperationShr) error
// compileRotl adds instructions to perform wazeroir.OperationRotl.
compileRotl(o wazeroir.OperationRotl) error
// compileRotr adds instructions to perform wazeroir.OperationRotr.
compileRotr(o wazeroir.OperationRotr) error
// compileNeg adds instructions to perform wazeroir.OperationAbs.
compileAbs(o wazeroir.OperationAbs) error
// compileNeg adds instructions to perform wazeroir.OperationNeg.
compileNeg(o wazeroir.OperationNeg) error
// compileCeil adds instructions to perform wazeroir.OperationCeil.
compileCeil(o wazeroir.OperationCeil) error
// compileFloor adds instructions to perform wazeroir.OperationFloor.
compileFloor(o wazeroir.OperationFloor) error
// compileTrunc adds instructions to perform wazeroir.OperationTrunc.
compileTrunc(o wazeroir.OperationTrunc) error
// compileNearest adds instructions to perform wazeroir.OperationNearest.
compileNearest(o wazeroir.OperationNearest) error
// compileSqrt adds instructions perform wazeroir.OperationSqrt.
compileSqrt(o wazeroir.OperationSqrt) error
// compileMin adds instructions perform wazeroir.OperationMin.
compileMin(o wazeroir.OperationMin) error
// compileMax adds instructions perform wazeroir.OperationMax.
compileMax(o wazeroir.OperationMax) error
// compileCopysign adds instructions to perform wazeroir.OperationCopysign.
compileCopysign(o wazeroir.OperationCopysign) error
// compileI32WrapFromI64 adds instructions to perform wazeroir.OperationI32WrapFromI64.
compileI32WrapFromI64() error
// compileITruncFromF adds instructions to perform wazeroir.OperationITruncFromF.
compileITruncFromF(o wazeroir.OperationITruncFromF) error
// compileFConvertFromI adds instructions to perform wazeroir.OperationFConvertFromI.
compileFConvertFromI(o wazeroir.OperationFConvertFromI) error
// compileF32DemoteFromF64 adds instructions to perform wazeroir.OperationF32DemoteFromF64.
compileF32DemoteFromF64() error
// compileF64PromoteFromF32 adds instructions to perform wazeroir.OperationF64PromoteFromF32.
compileF64PromoteFromF32() error
// compileI32ReinterpretFromF32 adds instructions to perform wazeroir.OperationI32ReinterpretFromF32.
compileI32ReinterpretFromF32() error
// compileI64ReinterpretFromF64 adds instructions to perform wazeroir.OperationI64ReinterpretFromF64.
compileI64ReinterpretFromF64() error
// compileF32ReinterpretFromI32 adds instructions to perform wazeroir.OperationF32ReinterpretFromI32.
compileF32ReinterpretFromI32() error
// compileF64ReinterpretFromI64 adds instructions to perform wazeroir.OperationF64ReinterpretFromI64.
compileF64ReinterpretFromI64() error
// compileExtend adds instructions to perform wazeroir.OperationExtend.
compileExtend(o wazeroir.OperationExtend) error
// compileEq adds instructions to perform wazeroir.OperationEq.
compileEq(o wazeroir.OperationEq) error
// compileEq adds instructions to perform wazeroir.OperationNe.
compileNe(o wazeroir.OperationNe) error
// compileEq adds instructions to perform wazeroir.OperationEqz.
compileEqz(o wazeroir.OperationEqz) error
// compileLt adds instructions to perform wazeroir.OperationLt.
compileLt(o wazeroir.OperationLt) error
// compileGt adds instructions to perform wazeroir.OperationGt.
compileGt(o wazeroir.OperationGt) error
// compileLe adds instructions to perform wazeroir.OperationLe.
compileLe(o wazeroir.OperationLe) error
// compileLe adds instructions to perform wazeroir.OperationGe.
compileGe(o wazeroir.OperationGe) error
// compileLoad adds instructions to perform wazeroir.OperationLoad.
compileLoad(o wazeroir.OperationLoad) error
// compileLoad8 adds instructions to perform wazeroir.OperationLoad8.
compileLoad8(o wazeroir.OperationLoad8) error
// compileLoad16 adds instructions to perform wazeroir.OperationLoad16.
compileLoad16(o wazeroir.OperationLoad16) error
// compileLoad32 adds instructions to perform wazeroir.OperationLoad32.
compileLoad32(o wazeroir.OperationLoad32) error
// compileStore adds instructions to perform wazeroir.OperationStore.
compileStore(o wazeroir.OperationStore) error
// compileStore8 adds instructions to perform wazeroir.OperationStore8.
compileStore8(o wazeroir.OperationStore8) error
// compileStore16 adds instructions to perform wazeroir.OperationStore16.
compileStore16(o wazeroir.OperationStore16) error
// compileStore32 adds instructions to perform wazeroir.OperationStore32.
compileStore32(o wazeroir.OperationStore32) error
// compileMemorySize adds instruction to perform wazeroir.OperationMemoryGrow.
compileMemoryGrow() error
// compileMemorySize adds instruction to perform wazeroir.OperationMemorySize.
compileMemorySize() error
// compileConstI32 adds instruction to perform wazeroir.OperationConstI32.
compileConstI32(o wazeroir.OperationConstI32) error
// compileConstI64 adds instruction to perform wazeroir.OperationConstI64.
compileConstI64(o wazeroir.OperationConstI64) error
// compileConstF32 adds instruction to perform wazeroir.OperationConstF32.
compileConstF32(o wazeroir.OperationConstF32) error
// compileConstF64 adds instruction to perform wazeroir.OperationConstF64.
compileConstF64(o wazeroir.OperationConstF64) error
// compileSignExtend32From8 adds instructions to perform wazeroir.OperationSignExtend32From8.
compileSignExtend32From8() error
// compileSignExtend32From16 adds instructions to perform wazeroir.OperationSignExtend32From16.
compileSignExtend32From16() error
// compileSignExtend64From8 adds instructions to perform wazeroir.OperationSignExtend64From8.
compileSignExtend64From8() error
// compileSignExtend64From16 adds instructions to perform wazeroir.OperationSignExtend64From16.
compileSignExtend64From16() error
// compileSignExtend64From32 adds instructions to perform wazeroir.OperationSignExtend64From32.
compileSignExtend64From32() error
// compileMemoryInit adds instructions to perform wazeroir.OperationMemoryInit.
compileMemoryInit(wazeroir.OperationMemoryInit) error
// compileDataDrop adds instructions to perform wazeroir.OperationDataDrop.
compileDataDrop(wazeroir.OperationDataDrop) error
// compileMemoryCopy adds instructions to perform wazeroir.OperationMemoryCopy.
compileMemoryCopy() error
// compileMemoryFill adds instructions to perform wazeroir.OperationMemoryFill.
compileMemoryFill() error
// compileTableInit adds instructions to perform wazeroir.OperationTableInit.
compileTableInit(wazeroir.OperationTableInit) error
// compileTableCopy adds instructions to perform wazeroir.OperationTableCopy.
compileTableCopy(wazeroir.OperationTableCopy) error
// compileElemDrop adds instructions to perform wazeroir.OperationElemDrop.
compileElemDrop(wazeroir.OperationElemDrop) error
// compileRefFunc adds instructions to perform wazeroir.OperationRefFunc.
compileRefFunc(wazeroir.OperationRefFunc) error
// compileTableGet adds instructions to perform wazeroir.OperationTableGet.
compileTableGet(wazeroir.OperationTableGet) error
// compileTableSet adds instructions to perform wazeroir.OperationTableSet.
compileTableSet(wazeroir.OperationTableSet) error
// compileTableGrow adds instructions to perform wazeroir.OperationTableGrow.
compileTableGrow(wazeroir.OperationTableGrow) error
// compileTableSize adds instructions to perform wazeroir.OperationTableSize.
compileTableSize(wazeroir.OperationTableSize) error
// compileTableFill adds instructions to perform wazeroir.OperationTableFill.
compileTableFill(wazeroir.OperationTableFill) error
// compileV128Const adds instructions to perform wazeroir.OperationV128Const.
compileV128Const(wazeroir.OperationV128Const) error
// compileV128Add adds instructions to perform wazeroir.OperationV128Add.
compileV128Add(o wazeroir.OperationV128Add) error
// compileV128Sub adds instructions to perform wazeroir.OperationV128Sub.
compileV128Sub(o wazeroir.OperationV128Sub) error
// compileV128Load adds instructions to perform wazeroir.OperationV128Load.
compileV128Load(o wazeroir.OperationV128Load) error
// compileV128LoadLane adds instructions to perform wazeroir.OperationV128LoadLane.
compileV128LoadLane(o wazeroir.OperationV128LoadLane) error
// compileV128Store adds instructions to perform wazeroir.OperationV128Store.
compileV128Store(o wazeroir.OperationV128Store) error
// compileV128StoreLane adds instructions to perform wazeroir.OperationV128StoreLane.
compileV128StoreLane(o wazeroir.OperationV128StoreLane) error
// compileV128ExtractLane adds instructions to perform wazeroir.OperationV128ExtractLane.
compileV128ExtractLane(o wazeroir.OperationV128ExtractLane) error
// compileV128ReplaceLane adds instructions to perform wazeroir.OperationV128ReplaceLane.
compileV128ReplaceLane(o wazeroir.OperationV128ReplaceLane) error
// compileV128Splat adds instructions to perform wazeroir.OperationV128Splat.
compileV128Splat(o wazeroir.OperationV128Splat) error
// compileV128Shuffle adds instructions to perform wazeroir.OperationV128Shuffle.
compileV128Shuffle(o wazeroir.OperationV128Shuffle) error
// compileV128Swizzle adds instructions to perform wazeroir.OperationV128Swizzle.
compileV128Swizzle(o wazeroir.OperationV128Swizzle) error
// compileV128AnyTrue adds instructions to perform wazeroir.OperationV128AnyTrue.
compileV128AnyTrue(o wazeroir.OperationV128AnyTrue) error
// compileV128AllTrue adds instructions to perform wazeroir.OperationV128AllTrue.
compileV128AllTrue(o wazeroir.OperationV128AllTrue) error
// compileV128BitMask adds instructions to perform wazeroir.OperationV128BitMask.
compileV128BitMask(wazeroir.OperationV128BitMask) error
// compileV128And adds instructions to perform wazeroir.OperationV128And.
compileV128And(wazeroir.OperationV128And) error
// compileV128Not adds instructions to perform wazeroir.OperationV128Not.
compileV128Not(wazeroir.OperationV128Not) error
// compileV128Or adds instructions to perform wazeroir.OperationV128Or.
compileV128Or(wazeroir.OperationV128Or) error
// compileV128Xor adds instructions to perform wazeroir.OperationV128Xor.
compileV128Xor(wazeroir.OperationV128Xor) error
// compileV128Bitselect adds instructions to perform wazeroir.OperationV128Bitselect.
compileV128Bitselect(wazeroir.OperationV128Bitselect) error
// compileV128AndNot adds instructions to perform wazeroir.OperationV128AndNot.
compileV128AndNot(wazeroir.OperationV128AndNot) error
// compileV128Shr adds instructions to perform wazeroir.OperationV128Shr.
compileV128Shr(wazeroir.OperationV128Shr) error
// compileV128Shl adds instructions to perform wazeroir.OperationV128Shl.
compileV128Shl(wazeroir.OperationV128Shl) error
// compileV128Cmp adds instructions to perform wazeroir.OperationV128Cmp.
compileV128Cmp(wazeroir.OperationV128Cmp) error
// compileV128AddSat adds instructions to perform wazeroir.OperationV128AddSat.
compileV128AddSat(wazeroir.OperationV128AddSat) error
// compileV128SubSat adds instructions to perform wazeroir.OperationV128SubSat.
compileV128SubSat(wazeroir.OperationV128SubSat) error
// compileV128Mul adds instructions to perform wazeroir.OperationV128Mul.
compileV128Mul(wazeroir.OperationV128Mul) error
// compileV128Div adds instructions to perform wazeroir.OperationV128Div.
compileV128Div(wazeroir.OperationV128Div) error
// compileV128Neg adds instructions to perform wazeroir.OperationV128Neg.
compileV128Neg(wazeroir.OperationV128Neg) error
// compileV128Sqrt adds instructions to perform wazeroir.OperationV128Sqrt.
compileV128Sqrt(wazeroir.OperationV128Sqrt) error
// compileV128Abs adds instructions to perform wazeroir.OperationV128Abs.
compileV128Abs(wazeroir.OperationV128Abs) error
// compileV128Popcnt adds instructions to perform wazeroir.OperationV128Popcnt.
compileV128Popcnt(wazeroir.OperationV128Popcnt) error
// compileV128Min adds instructions to perform wazeroir.OperationV128Min.
compileV128Min(wazeroir.OperationV128Min) error
// compileV128Max adds instructions to perform wazeroir.OperationV128Max.
compileV128Max(wazeroir.OperationV128Max) error
// compileV128AvgrU adds instructions to perform wazeroir.OperationV128AvgrU.
compileV128AvgrU(wazeroir.OperationV128AvgrU) error
// compileV128Pmin adds instructions to perform wazeroir.OperationV128Pmin.
compileV128Pmin(wazeroir.OperationV128Pmin) error
// compileV128Pmax adds instructions to perform wazeroir.OperationV128Pmax.
compileV128Pmax(wazeroir.OperationV128Pmax) error
// compileV128Ceil adds instructions to perform wazeroir.OperationV128Ceil.
compileV128Ceil(wazeroir.OperationV128Ceil) error
// compileV128Floor adds instructions to perform wazeroir.OperationV128Floor.
compileV128Floor(wazeroir.OperationV128Floor) error
// compileV128Trunc adds instructions to perform wazeroir.OperationV128Trunc.
compileV128Trunc(wazeroir.OperationV128Trunc) error
// compileV128Nearest adds instructions to perform wazeroir.OperationV128Nearest.
compileV128Nearest(wazeroir.OperationV128Nearest) error
// compileV128Extend adds instructions to perform wazeroir.OperationV128Extend.
compileV128Extend(wazeroir.OperationV128Extend) error
// compileV128ExtMul adds instructions to perform wazeroir.OperationV128ExtMul.
compileV128ExtMul(wazeroir.OperationV128ExtMul) error
// compileV128Q15mulrSatS adds instructions to perform wazeroir.OperationV128Q15mulrSatS.
compileV128Q15mulrSatS(wazeroir.OperationV128Q15mulrSatS) error
// compileV128ExtAddPairwise adds instructions to perform wazeroir.OperationV128ExtAddPairwise.
compileV128ExtAddPairwise(o wazeroir.OperationV128ExtAddPairwise) error
// compileV128FloatPromote adds instructions to perform wazeroir.OperationV128FloatPromote.
compileV128FloatPromote(o wazeroir.OperationV128FloatPromote) error
// compileV128FloatDemote adds instructions to perform wazeroir.OperationV128FloatDemote.
compileV128FloatDemote(o wazeroir.OperationV128FloatDemote) error
// compileV128FConvertFromI adds instructions to perform wazeroir.OperationV128FConvertFromI.
compileV128FConvertFromI(o wazeroir.OperationV128FConvertFromI) error
// compileV128Dot adds instructions to perform wazeroir.OperationV128Dot.
compileV128Dot(o wazeroir.OperationV128Dot) error
// compileV128Narrow adds instructions to perform wazeroir.OperationV128Narrow.
compileV128Narrow(o wazeroir.OperationV128Narrow) error
// compileV128ITruncSatFromF adds instructions to perform wazeroir.OperationV128ITruncSatFromF.
compileV128ITruncSatFromF(o wazeroir.OperationV128ITruncSatFromF) error
// compileBuiltinFunctionCheckExitCode adds instructions to perform wazeroir.OperationBuiltinFunctionCheckExitCode.
compileBuiltinFunctionCheckExitCode() error
// compileReleaseRegisterToStack adds instructions to write the value on a register back to memory stack region.
compileReleaseRegisterToStack(loc *runtimeValueLocation)
// compileLoadValueOnStackToRegister adds instructions to load the value located on the stack to the assigned register.
compileLoadValueOnStackToRegister(loc *runtimeValueLocation)
// maybeCompileMoveTopConditionalToGeneralPurposeRegister moves the top value on the stack
// if the value is located on a conditional register.
//
// This is usually called at the beginning of methods on compiler interface where we possibly
// compile instructions without saving the conditional register value.
// The compileXXX functions without calling this function is saving the conditional
// value to the stack or register by invoking compileEnsureOnRegister for the top.
maybeCompileMoveTopConditionalToGeneralPurposeRegister() error
// allocateRegister returns an unused register of the given type. The register will be taken
// either from the free register pool or by stealing a used register.
//
// Note: resulting registers will not be marked as used so the call site should
// mark it used if necessary.
allocateRegister(t registerType) (reg asm.Register, err error)
// runtimeValueLocationStack returns the current runtimeValueLocationStack of the compiler implementation.
runtimeValueLocationStack() *runtimeValueLocationStack
// pushRuntimeValueLocationOnRegister pushes a new runtimeValueLocation on a register `reg` and of the type `vt`.
pushRuntimeValueLocationOnRegister(reg asm.Register, vt runtimeValueType) (ret *runtimeValueLocation)
// pushRuntimeValueLocationOnRegister pushes a new vector value's runtimeValueLocation on a register `reg`.
pushVectorRuntimeValueLocationOnRegister(reg asm.Register) (lowerBitsLocation *runtimeValueLocation)
// compileNOP compiles NOP instruction and returns the corresponding asm.Node in the assembled native code.
// This is used to emit DWARF based stack traces.
compileNOP() asm.Node
}
@@ -0,0 +1,125 @@
package compiler
import (
"github.com/tetratelabs/wazero/internal/asm"
"github.com/tetratelabs/wazero/internal/wazeroir"
)
// compileDropRange adds instruction to drop the values on the target range
// in an architecture independent way.
func compileDropRange(c compiler, r *wazeroir.InclusiveRange) (err error) {
locationStack := c.runtimeValueLocationStack()
if r == nil {
return
} else if r.Start == 0 {
for i := 0; i <= r.End; i++ {
if loc := locationStack.pop(); loc.onRegister() {
locationStack.releaseRegister(loc)
}
}
return
}
// If the top value is alive, we must ensure that it is not located as a conditional.
// Otherwise, the conditional flag might end up modified by the following operation.
if err = c.maybeCompileMoveTopConditionalToGeneralPurposeRegister(); err != nil {
return
}
dropValues, liveValues := locationStack.dropsLivesForInclusiveRange(r)
// Frees all the registers used by drop target values.
for i := range dropValues {
dv := &dropValues[i]
if dv.onRegister() {
locationStack.releaseRegister(dv)
}
}
// These registers are not nil if a live value of that type is currently located on the memory stack.
// In order to migrate these values, we can use them below.
gpTmp, vecTmp, err := getTemporariesForStackedLiveValues(c, liveValues)
if err != nil {
return err
}
// Reset the stack pointer below the end.
locationStack.sp -= uint64(len(liveValues) + len(dropValues))
// Push back the live values again.
for i := range liveValues {
live := &liveValues[i]
migrateLiveValue(c, live, gpTmp, vecTmp)
}
return
}
// migrateLiveValue migrates the live value `live` into the top of the stack. It might be located on the stack
// and in that case, we have to load it into either `generalPurposeTmpReg` or `vectorTmpReg` temporarily, and
// write it back into the *new* stack location.
func migrateLiveValue(c compiler, live *runtimeValueLocation, generalPurposeTmpReg, vectorTmpReg asm.Register) {
if live.valueType == runtimeValueTypeV128Hi {
// Higher bits of vector was already handled together with the lower bits.
return
}
previouslyOnStack := live.onStack()
if previouslyOnStack {
// If the value is on the stack, load the value on the old location into the temporary value,
// and then write it back to the new memory location below.
switch live.getRegisterType() {
case registerTypeGeneralPurpose:
live.setRegister(generalPurposeTmpReg)
case registerTypeVector:
live.setRegister(vectorTmpReg)
}
// Load the value into tmp.
c.compileLoadValueOnStackToRegister(live)
}
var newLocation *runtimeValueLocation
if live.valueType == runtimeValueTypeV128Lo {
newLocation = c.pushVectorRuntimeValueLocationOnRegister(live.register)
} else {
newLocation = c.pushRuntimeValueLocationOnRegister(live.register, live.valueType)
}
if previouslyOnStack {
// This case, the location is on the temporary register. Therefore,
// we have to release the value there into the *new* memory location
// so that the tmp can be used for subsequent live value migrations.
c.compileReleaseRegisterToStack(newLocation)
}
}
func getTemporariesForStackedLiveValues(c compiler, liveValues []runtimeValueLocation) (gpTmp, vecTmp asm.Register, err error) {
gpTmp, vecTmp = asm.NilRegister, asm.NilRegister
for i := range liveValues {
l := &liveValues[i]
if l.onStack() {
if rt := l.getRegisterType(); rt == registerTypeGeneralPurpose && gpTmp == asm.NilRegister {
gpTmp, err = c.allocateRegister(registerTypeGeneralPurpose)
if err != nil {
return
}
} else if rt == registerTypeVector && vecTmp == asm.NilRegister {
vecTmp, err = c.allocateRegister(registerTypeVector)
if err != nil {
return
}
}
}
}
return
}
// dropsLivesForInclusiveRange returns the live and drop target values for the given wazeroir.InclusiveRange.
func (v *runtimeValueLocationStack) dropsLivesForInclusiveRange(
r *wazeroir.InclusiveRange,
) (dropValues, liveValues []runtimeValueLocation) {
// liveValues are must be pushed backed after dropping the target range.
liveValues = v.stack[v.sp-uint64(r.Start) : v.sp]
// dropValues are the values on the drop target range.
dropValues = v.stack[v.sp-uint64(r.End)-1 : v.sp-uint64(r.Start)]
return
}
@@ -0,0 +1,423 @@
package compiler
import (
"fmt"
"strings"
"github.com/tetratelabs/wazero/internal/asm"
"github.com/tetratelabs/wazero/internal/wasm"
)
var (
// unreservedGeneralPurposeRegisters contains unreserved general purpose registers of integer type.
unreservedGeneralPurposeRegisters []asm.Register
// unreservedVectorRegisters contains unreserved vector registers.
unreservedVectorRegisters []asm.Register
)
func isGeneralPurposeRegister(r asm.Register) bool {
return unreservedGeneralPurposeRegisters[0] <= r && r <= unreservedGeneralPurposeRegisters[len(unreservedGeneralPurposeRegisters)-1]
}
func isVectorRegister(r asm.Register) bool {
return unreservedVectorRegisters[0] <= r && r <= unreservedVectorRegisters[len(unreservedVectorRegisters)-1]
}
// runtimeValueLocation corresponds to each variable pushed onto the wazeroir (virtual) stack,
// and it has the information about where it exists in the physical machine.
// It might exist in registers, or maybe on in the non-virtual physical stack allocated in memory.
type runtimeValueLocation struct {
valueType runtimeValueType
// register is set to asm.NilRegister if the value is stored in the memory stack.
register asm.Register
// conditionalRegister is set to conditionalRegisterStateUnset if the value is not on the conditional register.
conditionalRegister asm.ConditionalRegisterState
// stackPointer is the location of this value in the memory stack at runtime,
stackPointer uint64
}
func (v *runtimeValueLocation) getRegisterType() (ret registerType) {
switch v.valueType {
case runtimeValueTypeI32, runtimeValueTypeI64:
ret = registerTypeGeneralPurpose
case runtimeValueTypeF32, runtimeValueTypeF64,
runtimeValueTypeV128Lo, runtimeValueTypeV128Hi:
ret = registerTypeVector
default:
panic("BUG")
}
return
}
type runtimeValueType byte
const (
runtimeValueTypeNone runtimeValueType = iota
runtimeValueTypeI32
runtimeValueTypeI64
runtimeValueTypeF32
runtimeValueTypeF64
runtimeValueTypeV128Lo
runtimeValueTypeV128Hi
)
func (r runtimeValueType) String() (ret string) {
switch r {
case runtimeValueTypeI32:
ret = "i32"
case runtimeValueTypeI64:
ret = "i64"
case runtimeValueTypeF32:
ret = "f32"
case runtimeValueTypeF64:
ret = "f64"
case runtimeValueTypeV128Lo:
ret = "v128.lo"
case runtimeValueTypeV128Hi:
ret = "v128.hi"
}
return
}
func (v *runtimeValueLocation) setRegister(reg asm.Register) {
v.register = reg
v.conditionalRegister = asm.ConditionalRegisterStateUnset
}
func (v *runtimeValueLocation) onRegister() bool {
return v.register != asm.NilRegister && v.conditionalRegister == asm.ConditionalRegisterStateUnset
}
func (v *runtimeValueLocation) onStack() bool {
return v.register == asm.NilRegister && v.conditionalRegister == asm.ConditionalRegisterStateUnset
}
func (v *runtimeValueLocation) onConditionalRegister() bool {
return v.conditionalRegister != asm.ConditionalRegisterStateUnset
}
func (v *runtimeValueLocation) String() string {
var location string
if v.onStack() {
location = fmt.Sprintf("stack(%d)", v.stackPointer)
} else if v.onConditionalRegister() {
location = fmt.Sprintf("conditional(%d)", v.conditionalRegister)
} else if v.onRegister() {
location = fmt.Sprintf("register(%s)", registerNameFn(v.register))
}
return fmt.Sprintf("{type=%s,location=%s}", v.valueType, location)
}
func newRuntimeValueLocationStack() runtimeValueLocationStack {
return runtimeValueLocationStack{
stack: make([]runtimeValueLocation, 10),
usedRegisters: map[asm.Register]struct{}{},
unreservedVectorRegisters: unreservedVectorRegisters,
unreservedGeneralPurposeRegisters: unreservedGeneralPurposeRegisters,
}
}
// runtimeValueLocationStack represents the wazeroir virtual stack
// where each item holds the location information about where it exists
// on the physical machine at runtime.
//
// Notably this is only used in the compilation phase, not runtime,
// and we change the state of this struct at every wazeroir operation we compile.
// In this way, we can see where the operands of an operation (for example,
// two variables for wazeroir add operation.) exist and check the necessity for
// moving the variable to registers to perform actual CPU instruction
// to achieve wazeroir's add operation.
type runtimeValueLocationStack struct {
// stack holds all the variables.
stack []runtimeValueLocation
// sp is the current stack pointer.
sp uint64
// usedRegisters stores the used registers.
usedRegisters map[asm.Register]struct{}
// stackPointerCeil tracks max(.sp) across the lifespan of this struct.
stackPointerCeil uint64
// unreservedGeneralPurposeRegisters and unreservedVectorRegisters hold
// architecture dependent unreserved register list.
unreservedGeneralPurposeRegisters, unreservedVectorRegisters []asm.Register
}
func (v *runtimeValueLocationStack) initialized() bool {
return len(v.unreservedGeneralPurposeRegisters) > 0
}
func (v *runtimeValueLocationStack) reset() {
v.stackPointerCeil, v.sp = 0, 0
v.stack = v.stack[:0]
v.usedRegisters = map[asm.Register]struct{}{}
}
func (v *runtimeValueLocationStack) String() string {
var stackStr []string
for i := uint64(0); i < v.sp; i++ {
stackStr = append(stackStr, v.stack[i].String())
}
var usedRegisters []string
for reg := range v.usedRegisters {
usedRegisters = append(usedRegisters, registerNameFn(reg))
}
return fmt.Sprintf("sp=%d, stack=[%s], used_registers=[%s]", v.sp, strings.Join(stackStr, ","), strings.Join(usedRegisters, ","))
}
func (v *runtimeValueLocationStack) clone() runtimeValueLocationStack {
ret := runtimeValueLocationStack{}
ret.sp = v.sp
ret.usedRegisters = make(map[asm.Register]struct{}, len(ret.usedRegisters))
for r := range v.usedRegisters {
ret.markRegisterUsed(r)
}
ret.stack = make([]runtimeValueLocation, len(v.stack))
copy(ret.stack, v.stack)
ret.stackPointerCeil = v.stackPointerCeil
ret.unreservedGeneralPurposeRegisters = v.unreservedGeneralPurposeRegisters
ret.unreservedVectorRegisters = v.unreservedVectorRegisters
return ret
}
// pushRuntimeValueLocationOnRegister creates a new runtimeValueLocation with a given register and pushes onto
// the location stack.
func (v *runtimeValueLocationStack) pushRuntimeValueLocationOnRegister(reg asm.Register, vt runtimeValueType) (loc *runtimeValueLocation) {
loc = v.push(reg, asm.ConditionalRegisterStateUnset)
loc.valueType = vt
return
}
// pushRuntimeValueLocationOnRegister creates a new runtimeValueLocation and pushes onto the location stack.
func (v *runtimeValueLocationStack) pushRuntimeValueLocationOnStack() (loc *runtimeValueLocation) {
loc = v.push(asm.NilRegister, asm.ConditionalRegisterStateUnset)
loc.valueType = runtimeValueTypeNone
return
}
// pushRuntimeValueLocationOnRegister creates a new runtimeValueLocation with a given conditional register state
// and pushes onto the location stack.
func (v *runtimeValueLocationStack) pushRuntimeValueLocationOnConditionalRegister(state asm.ConditionalRegisterState) (loc *runtimeValueLocation) {
loc = v.push(asm.NilRegister, state)
loc.valueType = runtimeValueTypeI32
return
}
// push a runtimeValueLocation onto the stack.
func (v *runtimeValueLocationStack) push(reg asm.Register, conditionalRegister asm.ConditionalRegisterState) (ret *runtimeValueLocation) {
if v.sp >= uint64(len(v.stack)) {
// This case we need to grow the stack capacity by appending the item,
// rather than indexing.
v.stack = append(v.stack, runtimeValueLocation{})
}
ret = &v.stack[v.sp]
ret.register, ret.conditionalRegister, ret.stackPointer = reg, conditionalRegister, v.sp
v.sp++
// stackPointerCeil must be set after sp is incremented since
// we skip the stack grow if len(stack) >= basePointer+stackPointerCeil.
if v.sp > v.stackPointerCeil {
v.stackPointerCeil = v.sp
}
return
}
func (v *runtimeValueLocationStack) pop() (loc *runtimeValueLocation) {
v.sp--
loc = &v.stack[v.sp]
return
}
func (v *runtimeValueLocationStack) popV128() (loc *runtimeValueLocation) {
v.sp -= 2
loc = &v.stack[v.sp]
return
}
func (v *runtimeValueLocationStack) peek() (loc *runtimeValueLocation) {
loc = &v.stack[v.sp-1]
return
}
func (v *runtimeValueLocationStack) releaseRegister(loc *runtimeValueLocation) {
v.markRegisterUnused(loc.register)
loc.register = asm.NilRegister
loc.conditionalRegister = asm.ConditionalRegisterStateUnset
}
func (v *runtimeValueLocationStack) markRegisterUnused(regs ...asm.Register) {
for _, reg := range regs {
delete(v.usedRegisters, reg)
}
}
func (v *runtimeValueLocationStack) markRegisterUsed(regs ...asm.Register) {
for _, reg := range regs {
v.usedRegisters[reg] = struct{}{}
}
}
type registerType byte
const (
registerTypeGeneralPurpose registerType = iota
// registerTypeVector represents a vector register which can be used for either scalar float
// operation or SIMD vector operation depending on the instruction by which the register is used.
//
// Note: In normal assembly language, scalar float and vector register have different notations as
// Vn is for vectors and Qn is for scalar floats on arm64 for example. But on physical hardware,
// they are placed on the same locations. (Qn means the lower 64-bit of Vn vector register on arm64).
//
// In wazero, for the sake of simplicity in the register allocation, we intentionally conflate these two types
// and delegate the decision to the assembler which is aware of the instruction types for which these registers are used.
registerTypeVector
)
func (tp registerType) String() (ret string) {
switch tp {
case registerTypeGeneralPurpose:
ret = "int"
case registerTypeVector:
ret = "vector"
}
return
}
// takeFreeRegister searches for unused registers. Any found are marked used and returned.
func (v *runtimeValueLocationStack) takeFreeRegister(tp registerType) (reg asm.Register, found bool) {
var targetRegs []asm.Register
switch tp {
case registerTypeVector:
targetRegs = v.unreservedVectorRegisters
case registerTypeGeneralPurpose:
targetRegs = v.unreservedGeneralPurposeRegisters
}
for _, candidate := range targetRegs {
if _, ok := v.usedRegisters[candidate]; ok {
continue
}
return candidate, true
}
return 0, false
}
func (v *runtimeValueLocationStack) takeFreeRegisters(tp registerType, num int) (regs []asm.Register, found bool) {
var targetRegs []asm.Register
switch tp {
case registerTypeVector:
targetRegs = v.unreservedVectorRegisters
case registerTypeGeneralPurpose:
targetRegs = v.unreservedGeneralPurposeRegisters
}
regs = make([]asm.Register, 0, num)
for _, candidate := range targetRegs {
if _, ok := v.usedRegisters[candidate]; ok {
continue
}
regs = append(regs, candidate)
if len(regs) == num {
found = true
break
}
}
return
}
// Search through the stack, and steal the register from the last used
// variable on the stack.
func (v *runtimeValueLocationStack) takeStealTargetFromUsedRegister(tp registerType) (*runtimeValueLocation, bool) {
for i := uint64(0); i < v.sp; i++ {
loc := &v.stack[i]
if loc.onRegister() {
switch tp {
case registerTypeVector:
if loc.valueType == runtimeValueTypeV128Hi {
panic("BUG: V128Hi must be above the corresponding V128Lo")
}
if isVectorRegister(loc.register) {
return loc, true
}
case registerTypeGeneralPurpose:
if isGeneralPurposeRegister(loc.register) {
return loc, true
}
}
}
}
return nil, false
}
// init sets up the runtimeValueLocationStack which reflects the state of
// the stack at the beginning of the function.
//
// See the diagram in callEngine.stack.
func (v *runtimeValueLocationStack) init(sig *wasm.FunctionType) {
for _, t := range sig.Params {
loc := v.pushRuntimeValueLocationOnStack()
switch t {
case wasm.ValueTypeI32:
loc.valueType = runtimeValueTypeI32
case wasm.ValueTypeI64, wasm.ValueTypeFuncref, wasm.ValueTypeExternref:
loc.valueType = runtimeValueTypeI64
case wasm.ValueTypeF32:
loc.valueType = runtimeValueTypeF32
case wasm.ValueTypeF64:
loc.valueType = runtimeValueTypeF64
case wasm.ValueTypeV128:
loc.valueType = runtimeValueTypeV128Lo
hi := v.pushRuntimeValueLocationOnStack()
hi.valueType = runtimeValueTypeV128Hi
default:
panic("BUG")
}
}
// If the len(results) > len(args), the slots for all results are reserved after
// arguments, so we reflect that into the location stack.
for i := 0; i < sig.ResultNumInUint64-sig.ParamNumInUint64; i++ {
_ = v.pushRuntimeValueLocationOnStack()
}
// Then push the control frame fields.
for i := 0; i < callFrameDataSizeInUint64; i++ {
loc := v.pushRuntimeValueLocationOnStack()
loc.valueType = runtimeValueTypeI64
}
}
// getCallFrameLocations returns each field of callFrame's runtime location.
//
// See the diagram in callEngine.stack.
func (v *runtimeValueLocationStack) getCallFrameLocations(sig *wasm.FunctionType) (
returnAddress, callerStackBasePointerInBytes, callerFunction *runtimeValueLocation,
) {
offset := callFrameOffset(sig)
return &v.stack[offset], &v.stack[offset+1], &v.stack[offset+2]
}
// pushCallFrame pushes a call frame's runtime locations onto the stack assuming that
// the function call parameters are already pushed there.
//
// See the diagram in callEngine.stack.
func (v *runtimeValueLocationStack) pushCallFrame(callTargetFunctionType *wasm.FunctionType) (
returnAddress, callerStackBasePointerInBytes, callerFunction *runtimeValueLocation,
) {
// If len(results) > len(args), we reserve the slots for the results below the call frame.
reservedSlotsBeforeCallFrame := callTargetFunctionType.ResultNumInUint64 - callTargetFunctionType.ParamNumInUint64
for i := 0; i < reservedSlotsBeforeCallFrame; i++ {
v.pushRuntimeValueLocationOnStack()
}
// Push the runtime location for each field of callFrame struct. Note that each of them has
// uint64 type, and therefore must be treated as runtimeValueTypeI64.
// callFrame.returnAddress
returnAddress = v.pushRuntimeValueLocationOnStack()
returnAddress.valueType = runtimeValueTypeI64
// callFrame.returnStackBasePointerInBytes
callerStackBasePointerInBytes = v.pushRuntimeValueLocationOnStack()
callerStackBasePointerInBytes.valueType = runtimeValueTypeI64
// callFrame.function
callerFunction = v.pushRuntimeValueLocationOnStack()
callerFunction.valueType = runtimeValueTypeI64
return
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,205 @@
package compiler
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"github.com/tetratelabs/wazero/internal/platform"
"github.com/tetratelabs/wazero/internal/u32"
"github.com/tetratelabs/wazero/internal/u64"
"github.com/tetratelabs/wazero/internal/wasm"
)
func (e *engine) deleteCodes(module *wasm.Module) {
e.mux.Lock()
defer e.mux.Unlock()
delete(e.codes, module.ID)
// Note: we do not call e.Cache.Delete, as the lifetime of
// the content is up to the implementation of extencache.Cache interface.
}
func (e *engine) addCodes(module *wasm.Module, codes []*code, withGoFunc bool) (err error) {
e.addCodesToMemory(module, codes)
if !withGoFunc {
err = e.addCodesToCache(module, codes)
}
return
}
func (e *engine) getCodes(module *wasm.Module) (codes []*code, ok bool, err error) {
codes, ok = e.getCodesFromMemory(module)
if ok {
return
}
codes, ok, err = e.getCodesFromCache(module)
if ok {
e.addCodesToMemory(module, codes)
}
return
}
func (e *engine) addCodesToMemory(module *wasm.Module, codes []*code) {
e.mux.Lock()
defer e.mux.Unlock()
e.codes[module.ID] = codes
}
func (e *engine) getCodesFromMemory(module *wasm.Module) (codes []*code, ok bool) {
e.mux.RLock()
defer e.mux.RUnlock()
codes, ok = e.codes[module.ID]
return
}
func (e *engine) addCodesToCache(module *wasm.Module, codes []*code) (err error) {
if e.fileCache == nil || module.IsHostModule {
return
}
err = e.fileCache.Add(module.ID, serializeCodes(e.wazeroVersion, codes))
return
}
func (e *engine) getCodesFromCache(module *wasm.Module) (codes []*code, hit bool, err error) {
if e.fileCache == nil || module.IsHostModule {
return
}
// Check if the entries exist in the external cache.
var cached io.ReadCloser
cached, hit, err = e.fileCache.Get(module.ID)
if !hit || err != nil {
return
}
// Otherwise, we hit the cache on external cache.
// We retrieve *code structures from `cached`.
var staleCache bool
// Note: cached.Close is ensured to be called in deserializeCodes.
codes, staleCache, err = deserializeCodes(e.wazeroVersion, cached)
if err != nil {
hit = false
return
} else if staleCache {
return nil, false, e.fileCache.Delete(module.ID)
}
for i, c := range codes {
c.indexInModule = wasm.Index(i)
c.sourceModule = module
}
return
}
var wazeroMagic = "WAZERO" // version must be synced with the tag of the wazero library.
func serializeCodes(wazeroVersion string, codes []*code) io.Reader {
buf := bytes.NewBuffer(nil)
// First 6 byte: WAZERO header.
buf.WriteString(wazeroMagic)
// Next 1 byte: length of version:
buf.WriteByte(byte(len(wazeroVersion)))
// Version of wazero.
buf.WriteString(wazeroVersion)
// Number of *code (== locally defined functions in the module): 4 bytes.
buf.Write(u32.LeBytes(uint32(len(codes))))
for _, c := range codes {
// The stack pointer ceil (8 bytes).
buf.Write(u64.LeBytes(c.stackPointerCeil))
// The length of code segment (8 bytes).
buf.Write(u64.LeBytes(uint64(len(c.codeSegment))))
// Append the native code.
buf.Write(c.codeSegment)
}
return bytes.NewReader(buf.Bytes())
}
func deserializeCodes(wazeroVersion string, reader io.ReadCloser) (codes []*code, staleCache bool, err error) {
defer reader.Close()
cacheHeaderSize := len(wazeroMagic) + 1 /* version size */ + len(wazeroVersion) + 4 /* number of functions */
// Read the header before the native code.
header := make([]byte, cacheHeaderSize)
n, err := reader.Read(header)
if err != nil {
return nil, false, fmt.Errorf("compilationcache: error reading header: %v", err)
}
if n != cacheHeaderSize {
return nil, false, fmt.Errorf("compilationcache: invalid header length: %d", n)
}
// Check the version compatibility.
versionSize := int(header[len(wazeroMagic)])
cachedVersionBegin, cachedVersionEnd := len(wazeroMagic)+1, len(wazeroMagic)+1+versionSize
if cachedVersionEnd >= len(header) {
staleCache = true
return
} else if cachedVersion := string(header[cachedVersionBegin:cachedVersionEnd]); cachedVersion != wazeroVersion {
staleCache = true
return
}
functionsNum := binary.LittleEndian.Uint32(header[len(header)-4:])
codes = make([]*code, 0, functionsNum)
var eightBytes [8]byte
var nativeCodeLen uint64
for i := uint32(0); i < functionsNum; i++ {
c := &code{}
// Read the stack pointer ceil.
if c.stackPointerCeil, err = readUint64(reader, &eightBytes); err != nil {
err = fmt.Errorf("compilationcache: error reading func[%d] stack pointer ceil: %v", i, err)
break
}
// Read (and mmap) the native code.
if nativeCodeLen, err = readUint64(reader, &eightBytes); err != nil {
err = fmt.Errorf("compilationcache: error reading func[%d] reading native code size: %v", i, err)
break
}
if c.codeSegment, err = platform.MmapCodeSegment(reader, int(nativeCodeLen)); err != nil {
err = fmt.Errorf("compilationcache: error mmapping func[%d] code (len=%d): %v", i, nativeCodeLen, err)
break
}
codes = append(codes, c)
}
if err != nil {
for _, c := range codes {
if errMunmap := platform.MunmapCodeSegment(c.codeSegment); errMunmap != nil {
// Munmap failure shouldn't happen.
panic(errMunmap)
}
}
codes = nil
}
return
}
// readUint64 strictly reads a uint64 in little-endian byte order, using the
// given array as a buffer. This returns io.EOF if less than 8 bytes were read.
func readUint64(reader io.Reader, b *[8]byte) (uint64, error) {
s := b[0:8]
n, err := reader.Read(s)
if err != nil {
return 0, err
} else if n < 8 { // more strict than reader.Read
return 0, io.EOF
}
// read the u64 from the underlying buffer
ret := binary.LittleEndian.Uint64(s)
// clear the underlying array
for i := 0; i < 8; i++ {
b[i] = 0
}
return ret, nil
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,42 @@
package filecache
import (
"crypto/sha256"
"io"
)
// Cache allows the compiler engine to skip compilation of wasm to machine code
// where doing so is redundant for the same wasm binary and version of wazero.
//
// This augments the default in-memory cache of compiled functions, by
// decoupling it from a wazero.Runtime instance. Concretely, a runtime loses
// its cache once closed. This cache allows the runtime to rebuild its
// in-memory cache quicker, significantly reducing first-hit penalty on a hit.
//
// See New for the example implementation.
type Cache interface {
// Get is called when the runtime is trying to get the cached compiled functions.
// Implementations are supposed to return compiled function in io.Reader with ok=true
// if the key exists on the cache. In the case of not-found, this should return
// ok=false with err=nil. content.Close() is automatically called by
// the caller of this Get.
//
// Note: the returned content won't go through the validation pass of Wasm binary
// which is applied when the binary is compiled from scratch without cache hit.
Get(key Key) (content io.ReadCloser, ok bool, err error)
//
// Add is called when the runtime is trying to add the new cache entry.
// The given `content` must be un-modified, and returned as-is in Get method.
//
// Note: the `content` is ensured to be safe through the validation phase applied on the Wasm binary.
Add(key Key, content io.Reader) (err error)
//
// Delete is called when the cache on the `key` returned by Get is no longer usable, and
// must be purged. Specifically, this is called happens when the wazero's version has been changed.
// For example, that is when there's a difference between the version of compiling wazero and the
// version of the currently used wazero.
Delete(key Key) (err error)
}
// Key represents the 256-bit unique identifier assigned to each cache entry.
type Key = [sha256.Size]byte
+91
View File
@@ -0,0 +1,91 @@
package filecache
import (
"encoding/hex"
"errors"
"io"
"os"
"path"
"sync"
)
// New returns a new Cache implemented by fileCache.
func New(dir string) Cache {
return newFileCache(dir)
}
func newFileCache(dir string) *fileCache {
return &fileCache{dirPath: dir}
}
// fileCache persists compiled functions into dirPath.
//
// Note: this can be expanded to do binary signing/verification, set TTL on each entry, etc.
type fileCache struct {
dirPath string
mux sync.RWMutex
}
type fileReadCloser struct {
*os.File
fc *fileCache
}
func (fc *fileCache) path(key Key) string {
return path.Join(fc.dirPath, hex.EncodeToString(key[:]))
}
func (fc *fileCache) Get(key Key) (content io.ReadCloser, ok bool, err error) {
// TODO: take lock per key for more efficiency vs the complexity of impl.
fc.mux.RLock()
unlock := fc.mux.RUnlock
defer func() {
if unlock != nil {
unlock()
}
}()
f, err := os.Open(fc.path(key))
if errors.Is(err, os.ErrNotExist) {
return nil, false, nil
} else if err != nil {
return nil, false, err
} else {
// Unlock is done inside the content.Close() at the call site.
unlock = nil
return &fileReadCloser{File: f, fc: fc}, true, nil
}
}
// Close wraps the os.File Close to release the read lock on fileCache.
func (f *fileReadCloser) Close() (err error) {
defer f.fc.mux.RUnlock()
err = f.File.Close()
return
}
func (fc *fileCache) Add(key Key, content io.Reader) (err error) {
// TODO: take lock per key for more efficiency vs the complexity of impl.
fc.mux.Lock()
defer fc.mux.Unlock()
file, err := os.Create(fc.path(key))
if err != nil {
return
}
defer file.Close()
_, err = io.Copy(file, content)
return
}
func (fc *fileCache) Delete(key Key) (err error) {
// TODO: take lock per key for more efficiency vs the complexity of impl.
fc.mux.Lock()
defer fc.mux.Unlock()
err = os.Remove(fc.path(key))
if errors.Is(err, os.ErrNotExist) {
err = nil
}
return
}
+29
View File
@@ -0,0 +1,29 @@
package ieee754
import (
"encoding/binary"
"io"
"math"
)
// DecodeFloat32 decodes a float32 in IEEE 754 binary representation.
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#floating-point%E2%91%A2
func DecodeFloat32(buf []byte) (float32, error) {
if len(buf) < 4 {
return 0, io.ErrUnexpectedEOF
}
raw := binary.LittleEndian.Uint32(buf[:4])
return math.Float32frombits(raw), nil
}
// DecodeFloat64 decodes a float64 in IEEE 754 binary representation.
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#floating-point%E2%91%A2
func DecodeFloat64(buf []byte) (float64, error) {
if len(buf) < 8 {
return 0, io.ErrUnexpectedEOF
}
raw := binary.LittleEndian.Uint64(buf)
return math.Float64frombits(raw), nil
}
+285
View File
@@ -0,0 +1,285 @@
package leb128
import (
"errors"
"fmt"
"io"
)
const (
maxVarintLen32 = 5
maxVarintLen33 = maxVarintLen32
maxVarintLen64 = 10
int33Mask int64 = 1 << 7
int33Mask2 = ^int33Mask
int33Mask3 = 1 << 6
int33Mask4 = 8589934591 // 2^33-1
int33Mask5 = 1 << 32
int33Mask6 = int33Mask4 + 1 // 2^33
int64Mask3 = 1 << 6
int64Mask4 = ^0
)
var (
errOverflow32 = errors.New("overflows a 32-bit integer")
errOverflow33 = errors.New("overflows a 33-bit integer")
errOverflow64 = errors.New("overflows a 64-bit integer")
)
// EncodeInt32 encodes the signed value into a buffer in LEB128 format
//
// See https://en.wikipedia.org/wiki/LEB128#Encode_signed_integer
func EncodeInt32(value int32) []byte {
return EncodeInt64(int64(value))
}
// EncodeInt64 encodes the signed value into a buffer in LEB128 format
//
// See https://en.wikipedia.org/wiki/LEB128#Encode_signed_integer
func EncodeInt64(value int64) (buf []byte) {
for {
// Take 7 remaining low-order bits from the value into b.
b := uint8(value & 0x7f)
// Extract the sign bit.
s := uint8(value & 0x40)
value >>= 7
// The encoding unsigned numbers is simpler as it only needs to check if the value is non-zero to tell if there
// are more bits to encode. Signed is a little more complicated as you have to double-check the sign bit.
// If either case, set the high-order bit to tell the reader there are more bytes in this int.
if (value != -1 || s == 0) && (value != 0 || s != 0) {
b |= 0x80
}
// Append b into the buffer
buf = append(buf, b)
if b&0x80 == 0 {
break
}
}
return buf
}
// EncodeUint32 encodes the value into a buffer in LEB128 format
//
// See https://en.wikipedia.org/wiki/LEB128#Encode_unsigned_integer
func EncodeUint32(value uint32) []byte {
return EncodeUint64(uint64(value))
}
// EncodeUint64 encodes the value into a buffer in LEB128 format
//
// See https://en.wikipedia.org/wiki/LEB128#Encode_unsigned_integer
func EncodeUint64(value uint64) (buf []byte) {
// This is effectively a do/while loop where we take 7 bits of the value and encode them until it is zero.
for {
// Take 7 remaining low-order bits from the value into b.
b := uint8(value & 0x7f)
value = value >> 7
// If there are remaining bits, the value won't be zero: Set the high-
// order bit to tell the reader there are more bytes in this uint.
if value != 0 {
b |= 0x80
}
// Append b into the buffer
buf = append(buf, b)
if b&0x80 == 0 {
return buf
}
}
}
type nextByte func(i int) (byte, error)
func DecodeUint32(r io.ByteReader) (ret uint32, bytesRead uint64, err error) {
return decodeUint32(func(_ int) (byte, error) { return r.ReadByte() })
}
func LoadUint32(buf []byte) (ret uint32, bytesRead uint64, err error) {
return decodeUint32(func(i int) (byte, error) {
if i >= len(buf) {
return 0, io.EOF
}
return buf[i], nil
})
}
func decodeUint32(next nextByte) (ret uint32, bytesRead uint64, err error) {
// Derived from https://github.com/golang/go/blob/go1.20/src/encoding/binary/varint.go
// with the modification on the overflow handling tailored for 32-bits.
var s uint32
for i := 0; i < maxVarintLen32; i++ {
b, err := next(i)
if err != nil {
return 0, 0, err
}
if b < 0x80 {
// Unused bits must be all zero.
if i == maxVarintLen32-1 && (b&0xf0) > 0 {
return 0, 0, errOverflow32
}
return ret | uint32(b)<<s, uint64(i) + 1, nil
}
ret |= (uint32(b) & 0x7f) << s
s += 7
}
return 0, 0, errOverflow32
}
func LoadUint64(buf []byte) (ret uint64, bytesRead uint64, err error) {
bufLen := len(buf)
if bufLen == 0 {
return 0, 0, io.EOF
}
// Derived from https://github.com/golang/go/blob/go1.20/src/encoding/binary/varint.go
var s uint64
for i := 0; i < maxVarintLen64; i++ {
if i >= bufLen {
return 0, 0, io.EOF
}
b := buf[i]
if b < 0x80 {
// Unused bits (non first bit) must all be zero.
if i == maxVarintLen64-1 && b > 1 {
return 0, 0, errOverflow64
}
return ret | uint64(b)<<s, uint64(i) + 1, nil
}
ret |= (uint64(b) & 0x7f) << s
s += 7
}
return 0, 0, errOverflow64
}
func DecodeInt32(r io.ByteReader) (ret int32, bytesRead uint64, err error) {
return decodeInt32(func(_ int) (byte, error) { return r.ReadByte() })
}
func LoadInt32(buf []byte) (ret int32, bytesRead uint64, err error) {
return decodeInt32(func(i int) (byte, error) {
if i >= len(buf) {
return 0, io.EOF
}
return buf[i], nil
})
}
func decodeInt32(next nextByte) (ret int32, bytesRead uint64, err error) {
var shift int
var b byte
for {
b, err = next(int(bytesRead))
if err != nil {
return 0, 0, fmt.Errorf("readByte failed: %w", err)
}
ret |= (int32(b) & 0x7f) << shift
shift += 7
bytesRead++
if b&0x80 == 0 {
if shift < 32 && (b&0x40) != 0 {
ret |= ^0 << shift
}
// Over flow checks.
// fixme: can be optimized.
if bytesRead > maxVarintLen32 {
return 0, 0, errOverflow32
} else if unused := b & 0b00110000; bytesRead == maxVarintLen32 && ret < 0 && unused != 0b00110000 {
return 0, 0, errOverflow32
} else if bytesRead == maxVarintLen32 && ret >= 0 && unused != 0x00 {
return 0, 0, errOverflow32
}
return
}
}
}
// DecodeInt33AsInt64 is a special cased decoder for wasm.BlockType which is encoded as a positive signed integer, yet
// still needs to fit the 32-bit range of allowed indices. Hence, this is 33, not 32-bit!
//
// See https://webassembly.github.io/spec/core/binary/instructions.html#control-instructions
func DecodeInt33AsInt64(r io.ByteReader) (ret int64, bytesRead uint64, err error) {
var shift int
var b int64
var rb byte
for shift < 35 {
rb, err = r.ReadByte()
if err != nil {
return 0, 0, fmt.Errorf("readByte failed: %w", err)
}
b = int64(rb)
ret |= (b & int33Mask2) << shift
shift += 7
bytesRead++
if b&int33Mask == 0 {
break
}
}
// fixme: can be optimized
if shift < 33 && (b&int33Mask3) == int33Mask3 {
ret |= int33Mask4 << shift
}
ret = ret & int33Mask4
// if 33rd bit == 1, we translate it as a corresponding signed-33bit minus value
if ret&int33Mask5 > 0 {
ret = ret - int33Mask6
}
// Over flow checks.
// fixme: can be optimized.
if bytesRead > maxVarintLen33 {
return 0, 0, errOverflow33
} else if unused := b & 0b00100000; bytesRead == maxVarintLen33 && ret < 0 && unused != 0b00100000 {
return 0, 0, errOverflow33
} else if bytesRead == maxVarintLen33 && ret >= 0 && unused != 0x00 {
return 0, 0, errOverflow33
}
return ret, bytesRead, nil
}
func DecodeInt64(r io.ByteReader) (ret int64, bytesRead uint64, err error) {
return decodeInt64(func(_ int) (byte, error) { return r.ReadByte() })
}
func LoadInt64(buf []byte) (ret int64, bytesRead uint64, err error) {
return decodeInt64(func(i int) (byte, error) {
if i >= len(buf) {
return 0, io.EOF
}
return buf[i], nil
})
}
func decodeInt64(next nextByte) (ret int64, bytesRead uint64, err error) {
var shift int
var b byte
for {
b, err = next(int(bytesRead))
if err != nil {
return 0, 0, fmt.Errorf("readByte failed: %w", err)
}
ret |= (int64(b) & 0x7f) << shift
shift += 7
bytesRead++
if b&0x80 == 0 {
if shift < 64 && (b&int64Mask3) == int64Mask3 {
ret |= int64Mask4 << shift
}
// Over flow checks.
// fixme: can be optimized.
if bytesRead > maxVarintLen64 {
return 0, 0, errOverflow64
} else if unused := b & 0b00111110; bytesRead == maxVarintLen64 && ret < 0 && unused != 0b00111110 {
return 0, 0, errOverflow64
} else if bytesRead == maxVarintLen64 && ret >= 0 && unused != 0x00 {
return 0, 0, errOverflow64
}
return
}
}
}
+271
View File
@@ -0,0 +1,271 @@
package moremath
import (
"math"
)
// https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/syntax/values.html#floating-point
const (
// F32CanonicalNaNBits is the 32-bit float where payload's MSB equals 1 and others are all zero.
F32CanonicalNaNBits = uint32(0x7fc0_0000)
// F32CanonicalNaNBitsMask can be used to judge the value `v` is canonical nan as "v&F32CanonicalNaNBitsMask == F32CanonicalNaNBits"
F32CanonicalNaNBitsMask = uint32(0x7fff_ffff)
// F64CanonicalNaNBits is the 64-bit float where payload's MSB equals 1 and others are all zero.
F64CanonicalNaNBits = uint64(0x7ff8_0000_0000_0000)
// F64CanonicalNaNBitsMask can be used to judge the value `v` is canonical nan as "v&F64CanonicalNaNBitsMask == F64CanonicalNaNBits"
F64CanonicalNaNBitsMask = uint64(0x7fff_ffff_ffff_ffff)
// F32ArithmeticNaNPayloadMSB is used to extract the most significant bit of payload of 32-bit arithmetic NaN values
F32ArithmeticNaNPayloadMSB = uint32(0x0040_0000)
// F32ExponentMask is used to extract the exponent of 32-bit floating point.
F32ExponentMask = uint32(0x7f80_0000)
// F32ArithmeticNaNBits is an example 32-bit arithmetic NaN.
F32ArithmeticNaNBits = F32CanonicalNaNBits | 0b1 // Set first bit to make this different from the canonical NaN.
// F64ArithmeticNaNPayloadMSB is used to extract the most significant bit of payload of 64-bit arithmetic NaN values
F64ArithmeticNaNPayloadMSB = uint64(0x0008_0000_0000_0000)
// F64ExponentMask is used to extract the exponent of 64-bit floating point.
F64ExponentMask = uint64(0x7ff0_0000_0000_0000)
// F64ArithmeticNaNBits is an example 64-bit arithmetic NaN.
F64ArithmeticNaNBits = F64CanonicalNaNBits | 0b1 // Set first bit to make this different from the canonical NaN.
)
// WasmCompatMin64 is the Wasm spec compatible variant of math.Min for 64-bit floating points.
func WasmCompatMin64(x, y float64) float64 {
switch {
case math.IsNaN(x) || math.IsNaN(y):
return returnF64NaNBinOp(x, y)
case math.IsInf(x, -1) || math.IsInf(y, -1):
return math.Inf(-1)
case x == 0 && x == y:
if math.Signbit(x) {
return x
}
return y
}
if x < y {
return x
}
return y
}
// WasmCompatMin32 is the Wasm spec compatible variant of math.Min for 32-bit floating points.
func WasmCompatMin32(x, y float32) float32 {
x64, y64 := float64(x), float64(y)
switch {
case math.IsNaN(x64) || math.IsNaN(y64):
return returnF32NaNBinOp(x, y)
case math.IsInf(x64, -1) || math.IsInf(y64, -1):
return float32(math.Inf(-1))
case x == 0 && x == y:
if math.Signbit(x64) {
return x
}
return y
}
if x < y {
return x
}
return y
}
// WasmCompatMax64 is the Wasm spec compatible variant of math.Max for 64-bit floating points.
func WasmCompatMax64(x, y float64) float64 {
switch {
case math.IsNaN(x) || math.IsNaN(y):
return returnF64NaNBinOp(x, y)
case math.IsInf(x, 1) || math.IsInf(y, 1):
return math.Inf(1)
case x == 0 && x == y:
if math.Signbit(x) {
return y
}
return x
}
if x > y {
return x
}
return y
}
// WasmCompatMax32 is the Wasm spec compatible variant of math.Max for 32-bit floating points.
func WasmCompatMax32(x, y float32) float32 {
x64, y64 := float64(x), float64(y)
switch {
case math.IsNaN(x64) || math.IsNaN(y64):
return returnF32NaNBinOp(x, y)
case math.IsInf(x64, 1) || math.IsInf(y64, 1):
return float32(math.Inf(1))
case x == 0 && x == y:
if math.Signbit(x64) {
return y
}
return x
}
if x > y {
return x
}
return y
}
// WasmCompatNearestF32 is the Wasm spec compatible variant of math.Round, used for Nearest instruction.
// For example, this converts 1.9 to 2.0, and this has the semantics of LLVM's rint intrinsic.
//
// e.g. math.Round(-4.5) results in -5 while this results in -4.
//
// See https://llvm.org/docs/LangRef.html#llvm-rint-intrinsic.
func WasmCompatNearestF32(f float32) float32 {
var res float32
// TODO: look at https://github.com/bytecodealliance/wasmtime/pull/2171 and reconsider this algorithm
if f != 0 {
ceil := float32(math.Ceil(float64(f)))
floor := float32(math.Floor(float64(f)))
distToCeil := math.Abs(float64(f - ceil))
distToFloor := math.Abs(float64(f - floor))
h := ceil / 2.0
if distToCeil < distToFloor {
res = ceil
} else if distToCeil == distToFloor && float32(math.Floor(float64(h))) == h {
res = ceil
} else {
res = floor
}
} else {
res = f
}
return returnF32UniOp(f, res)
}
// WasmCompatNearestF64 is the Wasm spec compatible variant of math.Round, used for Nearest instruction.
// For example, this converts 1.9 to 2.0, and this has the semantics of LLVM's rint intrinsic.
//
// e.g. math.Round(-4.5) results in -5 while this results in -4.
//
// See https://llvm.org/docs/LangRef.html#llvm-rint-intrinsic.
func WasmCompatNearestF64(f float64) float64 {
// TODO: look at https://github.com/bytecodealliance/wasmtime/pull/2171 and reconsider this algorithm
var res float64
if f != 0 {
ceil := math.Ceil(f)
floor := math.Floor(f)
distToCeil := math.Abs(f - ceil)
distToFloor := math.Abs(f - floor)
h := ceil / 2.0
if distToCeil < distToFloor {
res = ceil
} else if distToCeil == distToFloor && math.Floor(h) == h {
res = ceil
} else {
res = floor
}
} else {
res = f
}
return returnF64UniOp(f, res)
}
// WasmCompatCeilF32 is the same as math.Ceil on 32-bit except that
// the returned NaN value follows the Wasm specification on NaN
// propagation.
// https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/exec/numerics.html#nan-propagation
func WasmCompatCeilF32(f float32) float32 {
return returnF32UniOp(f, float32(math.Ceil(float64(f))))
}
// WasmCompatCeilF64 is the same as math.Ceil on 64-bit except that
// the returned NaN value follows the Wasm specification on NaN
// propagation.
// https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/exec/numerics.html#nan-propagation
func WasmCompatCeilF64(f float64) float64 {
return returnF64UniOp(f, math.Ceil(f))
}
// WasmCompatFloorF32 is the same as math.Floor on 32-bit except that
// the returned NaN value follows the Wasm specification on NaN
// propagation.
// https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/exec/numerics.html#nan-propagation
func WasmCompatFloorF32(f float32) float32 {
return returnF32UniOp(f, float32(math.Floor(float64(f))))
}
// WasmCompatFloorF64 is the same as math.Floor on 64-bit except that
// the returned NaN value follows the Wasm specification on NaN
// propagation.
// https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/exec/numerics.html#nan-propagation
func WasmCompatFloorF64(f float64) float64 {
return returnF64UniOp(f, math.Floor(f))
}
// WasmCompatTruncF32 is the same as math.Trunc on 32-bit except that
// the returned NaN value follows the Wasm specification on NaN
// propagation.
// https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/exec/numerics.html#nan-propagation
func WasmCompatTruncF32(f float32) float32 {
return returnF32UniOp(f, float32(math.Trunc(float64(f))))
}
// WasmCompatTruncF64 is the same as math.Trunc on 64-bit except that
// the returned NaN value follows the Wasm specification on NaN
// propagation.
// https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/exec/numerics.html#nan-propagation
func WasmCompatTruncF64(f float64) float64 {
return returnF64UniOp(f, math.Trunc(f))
}
func f32IsNaN(v float32) bool {
return v != v // this is how NaN is defined.
}
func f64IsNaN(v float64) bool {
return v != v // this is how NaN is defined.
}
// returnF32UniOp returns the result of 32-bit unary operation. This accepts `original` which is the operand,
// and `result` which is its result. This returns the `result` as-is if the result is not NaN. Otherwise, this follows
// the same logic as in the reference interpreter as well as the amd64 and arm64 floating point handling.
func returnF32UniOp(original, result float32) float32 {
// Following the same logic as in the reference interpreter:
// https://github.com/WebAssembly/spec/blob/d48af683f5e6d00c13f775ab07d29a15daf92203/interpreter/exec/fxx.ml#L115-L122
if !f32IsNaN(result) {
return result
}
if !f32IsNaN(original) {
return math.Float32frombits(F32CanonicalNaNBits)
}
return math.Float32frombits(math.Float32bits(original) | F32CanonicalNaNBits)
}
// returnF32UniOp returns the result of 64-bit unary operation. This accepts `original` which is the operand,
// and `result` which is its result. This returns the `result` as-is if the result is not NaN. Otherwise, this follows
// the same logic as in the reference interpreter as well as the amd64 and arm64 floating point handling.
func returnF64UniOp(original, result float64) float64 {
// Following the same logic as in the reference interpreter (== amd64 and arm64's behavior):
// https://github.com/WebAssembly/spec/blob/d48af683f5e6d00c13f775ab07d29a15daf92203/interpreter/exec/fxx.ml#L115-L122
if !f64IsNaN(result) {
return result
}
if !f64IsNaN(original) {
return math.Float64frombits(F64CanonicalNaNBits)
}
return math.Float64frombits(math.Float64bits(original) | F64CanonicalNaNBits)
}
// returnF64NaNBinOp returns a NaN for 64-bit binary operations. `x` and `y` are original floats
// and at least one of them is NaN. The returned NaN is guaranteed to comply with the NaN propagation
// procedure: https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/exec/numerics.html#nan-propagation
func returnF64NaNBinOp(x, y float64) float64 {
if f64IsNaN(x) {
return math.Float64frombits(math.Float64bits(x) | F64CanonicalNaNBits)
} else {
return math.Float64frombits(math.Float64bits(y) | F64CanonicalNaNBits)
}
}
// returnF64NaNBinOp returns a NaN for 32-bit binary operations. `x` and `y` are original floats
// and at least one of them is NaN. The returned NaN is guaranteed to comply with the NaN propagation
// procedure: https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/exec/numerics.html#nan-propagation
func returnF32NaNBinOp(x, y float32) float32 {
if f32IsNaN(x) {
return math.Float32frombits(math.Float32bits(x) | F32CanonicalNaNBits)
} else {
return math.Float32frombits(math.Float32bits(y) | F32CanonicalNaNBits)
}
}
+20
View File
@@ -0,0 +1,20 @@
package platform
// bufWriter implements io.Writer.
//
// This is implemented because bytes.Buffer cannot write from the beginning of the underlying buffer
// without changing the memory location. In this case, the underlying buffer is memory-mapped region,
// and we have to write into that region via io.Copy since sometimes the original native code exists
// as a file for external-cached cases.
type bufWriter struct {
underlying []byte
pos int
}
// Write implements io.Writer Write.
func (b *bufWriter) Write(p []byte) (n int, err error) {
copy(b.underlying[b.pos:], p)
n = len(p)
b.pos += n
return
}
+41
View File
@@ -0,0 +1,41 @@
package platform
import (
"io/fs"
"os"
"syscall"
)
// Chown is like os.Chown, except it returns a syscall.Errno, not a
// fs.PathError. For example, this returns syscall.ENOENT if the path doesn't
// exist. A syscall.Errno of zero is success.
//
// Note: This always returns syscall.ENOSYS on windows.
// See https://linux.die.net/man/3/chown
func Chown(path string, uid, gid int) syscall.Errno {
err := os.Chown(path, uid, gid)
return UnwrapOSError(err)
}
// Lchown is like os.Lchown, except it returns a syscall.Errno, not a
// fs.PathError. For example, this returns syscall.ENOENT if the path doesn't
// exist. A syscall.Errno of zero is success.
//
// Note: This always returns syscall.ENOSYS on windows.
// See https://linux.die.net/man/3/lchown
func Lchown(path string, uid, gid int) syscall.Errno {
err := os.Lchown(path, uid, gid)
return UnwrapOSError(err)
}
// ChownFile is like syscall.Fchown, but for nanosecond precision and
// fs.File instead of a file descriptor. This returns syscall.EBADF if the file
// or directory was closed. See https://linux.die.net/man/3/fchown
//
// Note: This always returns syscall.ENOSYS on windows.
func ChownFile(f fs.File, uid, gid int) syscall.Errno {
if f, ok := f.(fdFile); ok {
return fchown(f.Fd(), uid, gid)
}
return syscall.ENOSYS
}
+9
View File
@@ -0,0 +1,9 @@
//go:build !windows
package platform
import "syscall"
func fchown(fd uintptr, uid, gid int) syscall.Errno {
return UnwrapOSError(syscall.Fchown(int(fd), uid, gid))
}
@@ -0,0 +1,11 @@
//go:build windows
package platform
import "syscall"
// fchown is not supported on windows. For example, syscall.Fchown returns
// syscall.EWINDOWS, which is the same as syscall.ENOSYS.
func fchown(fd uintptr, uid, gid int) syscall.Errno {
return syscall.ENOSYS
}
+79
View File
@@ -0,0 +1,79 @@
package platform
const (
// CpuFeatureSSE3 is the flag to query CpuFeatureFlags.Has for SSEv3 capabilities
CpuFeatureSSE3 = uint64(1)
// CpuFeatureSSE4_1 is the flag to query CpuFeatureFlags.Has for SSEv4.1 capabilities
CpuFeatureSSE4_1 = uint64(1) << 19
// CpuFeatureSSE4_2 is the flag to query CpuFeatureFlags.Has for SSEv4.2 capabilities
CpuFeatureSSE4_2 = uint64(1) << 20
)
const (
// CpuExtraFeatureABM is the flag to query CpuFeatureFlags.HasExtra for Advanced Bit Manipulation capabilities (e.g. LZCNT)
CpuExtraFeatureABM = uint64(1) << 5
)
// CpuFeatures exposes the capabilities for this CPU, queried via the Has, HasExtra methods
var CpuFeatures CpuFeatureFlags = loadCpuFeatureFlags()
// CpuFeatureFlags exposes methods for querying CPU capabilities
type CpuFeatureFlags interface {
// Has returns true when the specified flag (represented as uint64) is supported
Has(cpuFeature uint64) bool
// HasExtra returns true when the specified extraFlag (represented as uint64) is supported
HasExtra(cpuFeature uint64) bool
}
// cpuFeatureFlags implements CpuFeatureFlags interface
type cpuFeatureFlags struct {
flags uint64
extraFlags uint64
}
// cpuid exposes the CPUID instruction to the Go layer (https://www.amd.com/system/files/TechDocs/25481.pdf)
// implemented in impl_amd64.s
func cpuid(arg1, arg2 uint32) (eax, ebx, ecx, edx uint32)
// cpuidAsBitmap combines the result of invoking cpuid to uint64 bitmap
func cpuidAsBitmap(arg1, arg2 uint32) uint64 {
_ /* eax */, _ /* ebx */, ecx, edx := cpuid(arg1, arg2)
return (uint64(edx) << 32) | uint64(ecx)
}
// loadStandardRange load flags from the standard range, panics otherwise
func loadStandardRange(id uint32) uint64 {
// ensure that the id is in the valid range, returned by cpuid(0,0)
maxRange, _, _, _ := cpuid(0, 0)
if id > maxRange {
panic("cannot query standard CPU flags")
}
return cpuidAsBitmap(id, 0)
}
// loadStandardRange load flags from the extended range, panics otherwise
func loadExtendedRange(id uint32) uint64 {
// ensure that the id is in the valid range, returned by cpuid(0x80000000,0)
maxRange, _, _, _ := cpuid(0x80000000, 0)
if id > maxRange {
panic("cannot query extended CPU flags")
}
return cpuidAsBitmap(id, 0)
}
func loadCpuFeatureFlags() CpuFeatureFlags {
return &cpuFeatureFlags{
flags: loadStandardRange(1),
extraFlags: loadExtendedRange(0x80000001),
}
}
// Has implements the same method on the CpuFeatureFlags interface
func (f *cpuFeatureFlags) Has(cpuFeature uint64) bool {
return (f.flags & cpuFeature) != 0
}
// HasExtra implements the same method on the CpuFeatureFlags interface
func (f *cpuFeatureFlags) HasExtra(cpuFeature uint64) bool {
return (f.extraFlags & cpuFeature) != 0
}
+14
View File
@@ -0,0 +1,14 @@
#include "textflag.h"
// lifted from github.com/intel-go/cpuid and src/internal/cpu/cpu_x86.s
// func cpuid(arg1, arg2 uint32) (eax, ebx, ecx, edx uint32)
TEXT ·cpuid(SB), NOSPLIT, $0-24
MOVL arg1+0(FP), AX
MOVL arg2+4(FP), CX
CPUID
MOVL AX, eax+8(FP)
MOVL BX, ebx+12(FP)
MOVL CX, ecx+16(FP)
MOVL DX, edx+20(FP)
RET
+17
View File
@@ -0,0 +1,17 @@
package platform
import (
"io"
"math/rand"
)
// seed is a fixed seed value for NewFakeRandSource.
//
// Trivia: While arbitrary, 42 was chosen as it is the "Ultimate Answer" in
// the Douglas Adams novel "The Hitchhiker's Guide to the Galaxy."
const seed = int64(42)
// NewFakeRandSource returns a deterministic source of random values.
func NewFakeRandSource() io.Reader {
return rand.New(rand.NewSource(seed))
}
Loaded 100 of 209 files, more files were not shown because too many files have changed in this diff. Show more