mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-08 08:04:14 -10:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d09c60089f | ||
|
|
a97d7caf55 |
No files matched your search
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
//go:build gosqlite
|
||||
//go:build go_sqlite
|
||||
|
||||
package db
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,83 @@
|
||||
# Go bindings to SQLite using Wazero
|
||||
|
||||
[](https://pkg.go.dev/github.com/ncruces/go-sqlite3)
|
||||
[](https://goreportcard.com/report/github.com/ncruces/go-sqlite3)
|
||||
[](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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,9 @@
|
||||
# Julian Day calculator
|
||||
|
||||
[](https://pkg.go.dev/github.com/ncruces/julianday)
|
||||
[](https://goreportcard.com/report/github.com/ncruces/julianday)
|
||||
[](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
@@ -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
@@ -0,0 +1,7 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
@@ -0,0 +1,2 @@
|
||||
# Improves experience of commands like `make format` on Windows
|
||||
* text=auto eol=lf
|
||||
+41
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,2 @@
|
||||
wazero
|
||||
Copyright 2020-2021 wazero authors
|
||||
+1017
File diff suppressed because it is too large.
Load diff
+131
@@ -0,0 +1,131 @@
|
||||
# wazero: the zero dependency WebAssembly runtime for Go developers
|
||||
|
||||
[](https://github.com/tetratelabs/wazero/actions/workflows/spectest.yaml) [](https://pkg.go.dev/github.com/tetratelabs/wazero) [](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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
}
|
||||
BIN
Binary file not shown.
+4
@@ -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
@@ -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.
|
||||
BIN
Binary file not shown.
+182
@@ -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
@@ -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
|
||||
}
|
||||
+1248
File diff suppressed because it is too large.
Load diff
+2716
File diff suppressed because it is too large.
Load diff
+193
@@ -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
@@ -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)
|
||||
}
|
||||
+1472
File diff suppressed because it is too large.
Load diff
+3889
File diff suppressed because it is too large.
Load diff
+184
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
+77
@@ -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
@@ -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
|
||||
+26
@@ -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()
|
||||
}
|
||||
+9
@@ -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.
|
||||
+51
@@ -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()
|
||||
}
|
||||
+21
@@ -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)
|
||||
+16
@@ -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))
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
//go:build !arm64 && !amd64
|
||||
|
||||
TEXT ·nativecall(SB), $0-16
|
||||
+334
@@ -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
|
||||
}
|
||||
+125
@@ -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
|
||||
}
|
||||
Generated
Vendored
+423
@@ -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
|
||||
}
|
||||
+1389
File diff suppressed because it is too large.
Load diff
+205
@@ -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
|
||||
}
|
||||
+5094
File diff suppressed because it is too large.
Load diff
+4246
File diff suppressed because it is too large.
Load diff
+2753
File diff suppressed because it is too large.
Load diff
+1531
File diff suppressed because it is too large.
Load diff
+4401
File diff suppressed because it is too large.
Load diff
+42
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
BIN
Binary file not shown.
+285
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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))
|
||||
}
|
||||
+11
@@ -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
@@ -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
@@ -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
@@ -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
Reference in new issue
Block a user