├── .gitignore ├── LICENSE.md ├── README.md ├── examples ├── basic-auth-middleware-tinygo │ ├── .gitignore │ ├── README.md │ ├── go │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go │ ├── package.json │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ └── wrangler.toml └── gray-scale-middleware-go │ ├── .gitignore │ ├── README.md │ ├── go │ ├── go.mod │ ├── go.sum │ └── main.go │ ├── package.json │ ├── src │ └── index.ts │ ├── tsconfig.json │ └── wrangler.toml ├── package.json ├── src ├── index.ts ├── wasm_exec_common.js ├── wasm_exec_go.js ├── wasm_exec_tinygo.js └── workers-go.ts ├── tsconfig.json └── wrangler.toml /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .wrangler 4 | .dev.vars 5 | 6 | # Change them to your taste: 7 | package-lock.json 8 | yarn.lock 9 | pnpm-lock.yaml 10 | 11 | # binary files 12 | *.wasm -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2024-present [syumai](https://github.com/syumai/) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hono-middleware-go 2 | 3 | * A package to write [Hono](https://github.com/honojs/hono) middleware in Go or TinyGo. 4 | * This package is based on [syumai/workers](https://github.com/syumai/workers). 5 | 6 | ## Installation 7 | 8 | ``` 9 | npm i @syumai/hono-middleware-go 10 | ``` 11 | 12 | ## Usage 13 | 14 | ### Write middleware in Go 15 | 16 | ```go 17 | package main 18 | 19 | import ( 20 | "io" 21 | "net/http" 22 | "strings" 23 | 24 | "github.com/syumai/workers/exp/hono" 25 | ) 26 | 27 | const ( 28 | userName = "user" 29 | userPassword = "password" 30 | ) 31 | 32 | func authenticate(req *http.Request) bool { 33 | username, password, ok := req.BasicAuth() 34 | return ok && username == userName && password == userPassword 35 | } 36 | 37 | func BasicAuth(c *hono.Context, next func()) { 38 | req := c.Request() 39 | if !authenticate(req) { 40 | c.SetHeader("WWW-Authenticate", `Basic realm="login is required"`) 41 | c.SetStatus(status) 42 | c.SetBody(io.NopCloser(strings.NewReader(msg + "\n"))) 43 | return 44 | } 45 | next() 46 | } 47 | 48 | func main() { 49 | hono.ServeMiddleware(BasicAuth) 50 | } 51 | ``` 52 | 53 | ### Use middleware in Hono 54 | 55 | ```ts 56 | import { Hono } from "hono"; 57 | import { tinygo } from "@syumai/hono-middleware-go"; 58 | import mod from "../wasm/middleware.wasm"; 59 | 60 | const app = new Hono(); 61 | 62 | app.use(tinygo(mod)); // or `go` for Go Wasm binary 63 | 64 | app.get("/", (c) => c.text("foo")); 65 | 66 | export default app; 67 | ``` 68 | 69 | ## Author 70 | 71 | syumai 72 | 73 | ## License 74 | 75 | MIT 76 | -------------------------------------------------------------------------------- /examples/basic-auth-middleware-tinygo/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .wrangler 4 | .dev.vars 5 | 6 | # Change them to your taste: 7 | package-lock.json 8 | yarn.lock 9 | pnpm-lock.yaml -------------------------------------------------------------------------------- /examples/basic-auth-middleware-tinygo/README.md: -------------------------------------------------------------------------------- 1 | # basic-auth-middleware-tinygo 2 | 3 | * This is an example of Hono middleware implemented in TinyGo. 4 | - a middleware of Basic-Auth . 5 | 6 | ## Demo 7 | 8 | * https://hono-middleware-tinygo-basic-auth.syumai.workers.dev/ 9 | * Try: 10 | - userName: `user` 11 | - password: `password` 12 | 13 | ## Development 14 | 15 | ### Requirements 16 | 17 | This project requires `tinygo` to be installed globally. 18 | 19 | ### Commands 20 | 21 | ``` 22 | npm run dev # run dev server 23 | npm run build-wasm # build TinyGo Wasm binary 24 | npm run deploy # deploy worker 25 | ``` 26 | -------------------------------------------------------------------------------- /examples/basic-auth-middleware-tinygo/go/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/syumai/hono-middleware-go/examples/basic-auth-proxy-tinygo/go 2 | 3 | go 1.22.3 4 | 5 | require github.com/syumai/workers v0.26.0 6 | -------------------------------------------------------------------------------- /examples/basic-auth-middleware-tinygo/go/go.sum: -------------------------------------------------------------------------------- 1 | github.com/syumai/workers v0.26.0 h1:icxE2mimM6138HUyXoz8nBjq2bC7YHPTSB8qAaHi31k= 2 | github.com/syumai/workers v0.26.0/go.mod h1:ZnqmdiHNBrbxOLrZ/HJ5jzHy6af9cmiNZk10R9NrIEA= 3 | -------------------------------------------------------------------------------- /examples/basic-auth-middleware-tinygo/go/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/syumai/workers/exp/hono" 9 | ) 10 | 11 | const ( 12 | userName = "user" 13 | userPassword = "password" 14 | ) 15 | 16 | func authenticate(req *http.Request) bool { 17 | username, password, ok := req.BasicAuth() 18 | return ok && username == userName && password == userPassword 19 | } 20 | 21 | func handleError(c *hono.Context, status int, msg string) { 22 | c.SetStatus(status) 23 | c.SetBody(io.NopCloser(strings.NewReader(msg + "\n"))) 24 | } 25 | 26 | func BasicAuth(c *hono.Context, next func()) { 27 | req := c.Request() 28 | if !authenticate(req) { 29 | c.SetHeader("WWW-Authenticate", `Basic realm="login is required"`) 30 | handleError(c, http.StatusUnauthorized, "Unauthorized") 31 | return 32 | } 33 | next() 34 | } 35 | 36 | func main() { 37 | hono.ServeMiddleware(BasicAuth) 38 | } 39 | -------------------------------------------------------------------------------- /examples/basic-auth-middleware-tinygo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build-wasm": "npm run build-wasm-tinygo", 4 | "build-wasm-tinygo": "mkdir -p wasm && cd go && tinygo build -o ../wasm/middleware.wasm -target wasm -no-debug ./...", 5 | "build-wasm-go": "mkdir -p wasm && cd go && GOOS=js GOARCH=wasm go build -o ../wasm/middleware.wasm .", 6 | "dev": "wrangler dev src/index.ts", 7 | "deploy": "wrangler deploy --minify src/index.ts" 8 | }, 9 | "dependencies": { 10 | "@syumai/hono-middleware-go": "file:../..", 11 | "hono": "^4.0.0" 12 | }, 13 | "devDependencies": { 14 | "@cloudflare/workers-types": "^4.20240129.0", 15 | "wrangler": "^3.25.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/basic-auth-middleware-tinygo/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { tinygo } from "@syumai/hono-middleware-go"; 3 | import mod from "../wasm/middleware.wasm"; 4 | 5 | const app = new Hono(); 6 | 7 | app.use(tinygo(mod)); 8 | 9 | app.get("/", (c) => { 10 | return c.text("Hello Hono!"); 11 | }); 12 | 13 | export default app; 14 | -------------------------------------------------------------------------------- /examples/basic-auth-middleware-tinygo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "Bundler", 6 | "strict": true, 7 | "lib": [ 8 | "ESNext" 9 | ], 10 | "types": [ 11 | "@cloudflare/workers-types" 12 | ], 13 | "jsx": "react-jsx", 14 | "jsxImportSource": "hono/jsx" 15 | }, 16 | } -------------------------------------------------------------------------------- /examples/basic-auth-middleware-tinygo/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "hono-middleware-tinygo-basic-auth" 2 | compatibility_date = "2023-12-01" 3 | -------------------------------------------------------------------------------- /examples/gray-scale-middleware-go/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .wrangler 4 | .dev.vars 5 | 6 | # Change them to your taste: 7 | package-lock.json 8 | yarn.lock 9 | pnpm-lock.yaml -------------------------------------------------------------------------------- /examples/gray-scale-middleware-go/README.md: -------------------------------------------------------------------------------- 1 | # gray-scale-middleware-go 2 | 3 | * This is an example of Hono middleware implemented in Go. 4 | - a middleware to convert PNG to Gray-scaled PNG. 5 | 6 | ## Demo 7 | 8 | * https://hono-middleware-go-gray-scale.syumai.workers.dev/image 9 | * https://hono-middleware-go-gray-scale.syumai.workers.dev/image-gray 10 | 11 | ## Development 12 | 13 | ### Requirements 14 | 15 | This project requires `go` to be installed globally. 16 | 17 | ### Commands 18 | 19 | ``` 20 | npm run dev # run dev server 21 | npm run build-wasm # build Go Wasm binary 22 | npm run deploy # deploy worker 23 | ``` 24 | -------------------------------------------------------------------------------- /examples/gray-scale-middleware-go/go/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/syumai/hono-middleware-go/examples/gray-scale-middleware-go/go 2 | 3 | go 1.22.3 4 | 5 | require github.com/syumai/workers v0.26.0 6 | -------------------------------------------------------------------------------- /examples/gray-scale-middleware-go/go/go.sum: -------------------------------------------------------------------------------- 1 | github.com/syumai/workers v0.26.0 h1:icxE2mimM6138HUyXoz8nBjq2bC7YHPTSB8qAaHi31k= 2 | github.com/syumai/workers v0.26.0/go.mod h1:ZnqmdiHNBrbxOLrZ/HJ5jzHy6af9cmiNZk10R9NrIEA= 3 | -------------------------------------------------------------------------------- /examples/gray-scale-middleware-go/go/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "image" 6 | "image/draw" 7 | "image/png" 8 | "io" 9 | "net/http" 10 | "strings" 11 | "syscall/js" 12 | 13 | "github.com/syumai/workers/exp/hono" 14 | ) 15 | 16 | func GrayScale(c *hono.Context, next func()) { 17 | next() 18 | img, _, err := image.Decode(c.ResponseBody()) 19 | baseResp := c.RawResponse() 20 | c.SetResponse(js.Undefined()) // clean up the previous response 21 | if err != nil { 22 | respObj := hono.NewJSResponse( 23 | io.NopCloser(strings.NewReader("invalid image format")), 24 | http.StatusBadRequest, 25 | nil, 26 | ) 27 | c.SetResponse(respObj) 28 | return 29 | } 30 | 31 | result := image.NewGray(img.Bounds()) 32 | draw.Draw(result, result.Bounds(), img, img.Bounds().Min, draw.Src) 33 | 34 | var buf bytes.Buffer 35 | if err = png.Encode(&buf, result); err != nil { 36 | respObj := hono.NewJSResponse( 37 | io.NopCloser(strings.NewReader("failed to encode image")), 38 | http.StatusBadRequest, 39 | nil, 40 | ) 41 | c.SetResponse(respObj) 42 | return 43 | } 44 | respObj := hono.NewJSResponseWithBase( 45 | io.NopCloser(&buf), 46 | baseResp, 47 | ) 48 | c.SetResponse(respObj) 49 | } 50 | 51 | func main() { 52 | hono.ServeMiddleware(GrayScale) 53 | } 54 | -------------------------------------------------------------------------------- /examples/gray-scale-middleware-go/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build-wasm": "npm run build-wasm-go", 4 | "build-wasm-tinygo": "mkdir -p wasm && cd go && tinygo build -o ../wasm/middleware.wasm -target wasm -no-debug ./...", 5 | "build-wasm-go": "mkdir -p wasm && cd go && GOOS=js GOARCH=wasm go build -o ../wasm/middleware.wasm .", 6 | "dev": "wrangler dev src/index.ts", 7 | "deploy": "wrangler deploy --minify src/index.ts" 8 | }, 9 | "dependencies": { 10 | "@syumai/hono-middleware-go": "file:../..", 11 | "hono": "^4.0.0" 12 | }, 13 | "devDependencies": { 14 | "@cloudflare/workers-types": "^4.20240129.0", 15 | "wrangler": "^3.25.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/gray-scale-middleware-go/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { go } from "@syumai/hono-middleware-go"; 3 | import mod from "../wasm/middleware.wasm"; 4 | 5 | const app = new Hono(); 6 | 7 | const imagePath = "https://syum.ai/image"; 8 | 9 | app.get("/image", (c) => { 10 | return fetch(imagePath); 11 | }); 12 | 13 | app.get("/image-gray", go(mod), () => { 14 | return fetch(imagePath); 15 | }); 16 | 17 | export default app; 18 | -------------------------------------------------------------------------------- /examples/gray-scale-middleware-go/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "Bundler", 6 | "strict": true, 7 | "lib": [ 8 | "ESNext" 9 | ], 10 | "types": [ 11 | "@cloudflare/workers-types" 12 | ], 13 | "jsx": "react-jsx", 14 | "jsxImportSource": "hono/jsx" 15 | }, 16 | } -------------------------------------------------------------------------------- /examples/gray-scale-middleware-go/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "hono-middleware-go-gray-scale" 2 | compatibility_date = "2023-12-01" 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@syumai/hono-middleware-go", 3 | "version": "0.1.2", 4 | "description": "A middleware for Hono runnning Go", 5 | "main": "dist/index.js", 6 | "type": "module", 7 | "types": "dist/index.d.ts", 8 | "files": [ 9 | "dist" 10 | ], 11 | "scripts": { 12 | "build": "tsup ./src/index.ts --format esm,cjs --dts --external 'cloudflare:sockets'", 13 | "publint": "publint", 14 | "release": "npm run build && npm run publint && npm publish" 15 | }, 16 | "exports": { 17 | ".": { 18 | "types": "./dist/index.d.mts", 19 | "import": "./dist/index.mjs", 20 | "require": "./dist/index.js" 21 | } 22 | }, 23 | "license": "MIT", 24 | "publishConfig": { 25 | "registry": "https://registry.npmjs.org", 26 | "access": "public" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/syumai/hono-middleware-go.git" 31 | }, 32 | "peerDependencies": { 33 | "hono": "*" 34 | }, 35 | "devDependencies": { 36 | "@cloudflare/workers-types": "^4.20240512.0", 37 | "hono": "^4.3.7", 38 | "publint": "^0.2.7", 39 | "tsup": "^8.0.2", 40 | "typescript": "^5.4.5", 41 | "wrangler": "^3.57.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { MiddlewareHandler, Next } from "hono"; 2 | import { createRuntimeContext, run } from "./workers-go.js"; 3 | 4 | type Binding = { 5 | runHonoMiddleware(next: Next): Promise; 6 | }; 7 | 8 | export const go = (mod: WebAssembly.Module): MiddlewareHandler => { 9 | return async function go(ctx, next) { 10 | const binding = {} as Binding; 11 | await run(createRuntimeContext(ctx.env, ctx, binding), mod, false); 12 | await binding.runHonoMiddleware(next); 13 | }; 14 | }; 15 | 16 | export const tinygo = (mod: WebAssembly.Module): MiddlewareHandler => { 17 | return async function tinygo(ctx, next) { 18 | const binding = {} as Binding; 19 | await run(createRuntimeContext(ctx.env, ctx, binding), mod, true); 20 | await binding.runHonoMiddleware(next); 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /src/wasm_exec_common.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | "use strict"; 6 | 7 | (() => { 8 | const decoder = new TextDecoder("utf-8"); 9 | 10 | const enosys = () => { 11 | const err = new Error("not implemented"); 12 | err.code = "ENOSYS"; 13 | return err; 14 | }; 15 | 16 | if (!globalThis.fs) { 17 | let outputBuf = ""; 18 | globalThis.fs = { 19 | constants: { 20 | O_WRONLY: -1, 21 | O_RDWR: -1, 22 | O_CREAT: -1, 23 | O_TRUNC: -1, 24 | O_APPEND: -1, 25 | O_EXCL: -1, 26 | }, // unused 27 | writeSync(fd, buf) { 28 | outputBuf += decoder.decode(buf); 29 | const nl = outputBuf.lastIndexOf("\n"); 30 | if (nl != -1) { 31 | console.log(outputBuf.substring(0, nl)); 32 | outputBuf = outputBuf.substring(nl + 1); 33 | } 34 | return buf.length; 35 | }, 36 | write(fd, buf, offset, length, position, callback) { 37 | if (offset !== 0 || length !== buf.length || position !== null) { 38 | callback(enosys()); 39 | return; 40 | } 41 | const n = this.writeSync(fd, buf); 42 | callback(null, n); 43 | }, 44 | chmod(path, mode, callback) { 45 | callback(enosys()); 46 | }, 47 | chown(path, uid, gid, callback) { 48 | callback(enosys()); 49 | }, 50 | close(fd, callback) { 51 | callback(enosys()); 52 | }, 53 | fchmod(fd, mode, callback) { 54 | callback(enosys()); 55 | }, 56 | fchown(fd, uid, gid, callback) { 57 | callback(enosys()); 58 | }, 59 | fstat(fd, callback) { 60 | callback(enosys()); 61 | }, 62 | fsync(fd, callback) { 63 | callback(null); 64 | }, 65 | ftruncate(fd, length, callback) { 66 | callback(enosys()); 67 | }, 68 | lchown(path, uid, gid, callback) { 69 | callback(enosys()); 70 | }, 71 | link(path, link, callback) { 72 | callback(enosys()); 73 | }, 74 | lstat(path, callback) { 75 | callback(enosys()); 76 | }, 77 | mkdir(path, perm, callback) { 78 | callback(enosys()); 79 | }, 80 | open(path, flags, mode, callback) { 81 | callback(enosys()); 82 | }, 83 | read(fd, buffer, offset, length, position, callback) { 84 | callback(enosys()); 85 | }, 86 | readdir(path, callback) { 87 | callback(enosys()); 88 | }, 89 | readlink(path, callback) { 90 | callback(enosys()); 91 | }, 92 | rename(from, to, callback) { 93 | callback(enosys()); 94 | }, 95 | rmdir(path, callback) { 96 | callback(enosys()); 97 | }, 98 | stat(path, callback) { 99 | callback(enosys()); 100 | }, 101 | symlink(path, link, callback) { 102 | callback(enosys()); 103 | }, 104 | truncate(path, length, callback) { 105 | callback(enosys()); 106 | }, 107 | unlink(path, callback) { 108 | callback(enosys()); 109 | }, 110 | utimes(path, atime, mtime, callback) { 111 | callback(enosys()); 112 | }, 113 | }; 114 | } 115 | 116 | if (!globalThis.process) { 117 | globalThis.process = { 118 | getuid() { 119 | return -1; 120 | }, 121 | getgid() { 122 | return -1; 123 | }, 124 | geteuid() { 125 | return -1; 126 | }, 127 | getegid() { 128 | return -1; 129 | }, 130 | getgroups() { 131 | throw enosys(); 132 | }, 133 | pid: -1, 134 | ppid: -1, 135 | umask() { 136 | throw enosys(); 137 | }, 138 | cwd() { 139 | throw enosys(); 140 | }, 141 | chdir() { 142 | throw enosys(); 143 | }, 144 | }; 145 | } 146 | 147 | if (!globalThis.crypto) { 148 | throw new Error( 149 | "globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)" 150 | ); 151 | } 152 | 153 | if (!globalThis.performance) { 154 | throw new Error( 155 | "globalThis.performance is not available, polyfill required (performance.now only)" 156 | ); 157 | } 158 | 159 | if (!globalThis.TextEncoder) { 160 | throw new Error( 161 | "globalThis.TextEncoder is not available, polyfill required" 162 | ); 163 | } 164 | 165 | if (!globalThis.TextDecoder) { 166 | throw new Error( 167 | "globalThis.TextDecoder is not available, polyfill required" 168 | ); 169 | } 170 | })(); 171 | -------------------------------------------------------------------------------- /src/wasm_exec_go.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | "use strict"; 6 | 7 | (() => { 8 | const encoder = new TextEncoder("utf-8"); 9 | const decoder = new TextDecoder("utf-8"); 10 | 11 | globalThis.Go = class { 12 | constructor() { 13 | this.argv = ["js"]; 14 | this.env = {}; 15 | this.exit = (code) => { 16 | if (code !== 0) { 17 | console.warn("exit code:", code); 18 | } 19 | }; 20 | this._exitPromise = new Promise((resolve) => { 21 | this._resolveExitPromise = resolve; 22 | }); 23 | this._pendingEvent = null; 24 | this._scheduledTimeouts = new Map(); 25 | this._nextCallbackTimeoutID = 1; 26 | 27 | const setInt64 = (addr, v) => { 28 | this.mem.setUint32(addr + 0, v, true); 29 | this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); 30 | }; 31 | 32 | const setInt32 = (addr, v) => { 33 | this.mem.setUint32(addr + 0, v, true); 34 | }; 35 | 36 | const getInt64 = (addr) => { 37 | const low = this.mem.getUint32(addr + 0, true); 38 | const high = this.mem.getInt32(addr + 4, true); 39 | return low + high * 4294967296; 40 | }; 41 | 42 | const loadValue = (addr) => { 43 | const f = this.mem.getFloat64(addr, true); 44 | if (f === 0) { 45 | return undefined; 46 | } 47 | if (!isNaN(f)) { 48 | return f; 49 | } 50 | 51 | const id = this.mem.getUint32(addr, true); 52 | return this._values[id]; 53 | }; 54 | 55 | const storeValue = (addr, v) => { 56 | const nanHead = 0x7ff80000; 57 | 58 | if (typeof v === "number" && v !== 0) { 59 | if (isNaN(v)) { 60 | this.mem.setUint32(addr + 4, nanHead, true); 61 | this.mem.setUint32(addr, 0, true); 62 | return; 63 | } 64 | this.mem.setFloat64(addr, v, true); 65 | return; 66 | } 67 | 68 | if (v === undefined) { 69 | this.mem.setFloat64(addr, 0, true); 70 | return; 71 | } 72 | 73 | let id = this._ids.get(v); 74 | if (id === undefined) { 75 | id = this._idPool.pop(); 76 | if (id === undefined) { 77 | id = this._values.length; 78 | } 79 | this._values[id] = v; 80 | this._goRefCounts[id] = 0; 81 | this._ids.set(v, id); 82 | } 83 | this._goRefCounts[id]++; 84 | let typeFlag = 0; 85 | switch (typeof v) { 86 | case "object": 87 | if (v !== null) { 88 | typeFlag = 1; 89 | } 90 | break; 91 | case "string": 92 | typeFlag = 2; 93 | break; 94 | case "symbol": 95 | typeFlag = 3; 96 | break; 97 | case "function": 98 | typeFlag = 4; 99 | break; 100 | } 101 | this.mem.setUint32(addr + 4, nanHead | typeFlag, true); 102 | this.mem.setUint32(addr, id, true); 103 | }; 104 | 105 | const loadSlice = (addr) => { 106 | const array = getInt64(addr + 0); 107 | const len = getInt64(addr + 8); 108 | return new Uint8Array(this._inst.exports.mem.buffer, array, len); 109 | }; 110 | 111 | const loadSliceOfValues = (addr) => { 112 | const array = getInt64(addr + 0); 113 | const len = getInt64(addr + 8); 114 | const a = new Array(len); 115 | for (let i = 0; i < len; i++) { 116 | a[i] = loadValue(array + i * 8); 117 | } 118 | return a; 119 | }; 120 | 121 | const loadString = (addr) => { 122 | const saddr = getInt64(addr + 0); 123 | const len = getInt64(addr + 8); 124 | return decoder.decode( 125 | new DataView(this._inst.exports.mem.buffer, saddr, len) 126 | ); 127 | }; 128 | 129 | const timeOrigin = Date.now() - performance.now(); 130 | this.importObject = { 131 | _gotest: { 132 | add: (a, b) => a + b, 133 | }, 134 | gojs: { 135 | // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) 136 | // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported 137 | // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). 138 | // This changes the SP, thus we have to update the SP used by the imported function. 139 | 140 | // func wasmExit(code int32) 141 | "runtime.wasmExit": (sp) => { 142 | sp >>>= 0; 143 | const code = this.mem.getInt32(sp + 8, true); 144 | this.exited = true; 145 | delete this._inst; 146 | delete this._values; 147 | delete this._goRefCounts; 148 | delete this._ids; 149 | delete this._idPool; 150 | this.exit(code); 151 | }, 152 | 153 | // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) 154 | "runtime.wasmWrite": (sp) => { 155 | sp >>>= 0; 156 | const fd = getInt64(sp + 8); 157 | const p = getInt64(sp + 16); 158 | const n = this.mem.getInt32(sp + 24, true); 159 | fs.writeSync( 160 | fd, 161 | new Uint8Array(this._inst.exports.mem.buffer, p, n) 162 | ); 163 | }, 164 | 165 | // func resetMemoryDataView() 166 | "runtime.resetMemoryDataView": (sp) => { 167 | sp >>>= 0; 168 | this.mem = new DataView(this._inst.exports.mem.buffer); 169 | }, 170 | 171 | // func nanotime1() int64 172 | "runtime.nanotime1": (sp) => { 173 | sp >>>= 0; 174 | setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); 175 | }, 176 | 177 | // func walltime() (sec int64, nsec int32) 178 | "runtime.walltime": (sp) => { 179 | sp >>>= 0; 180 | const msec = new Date().getTime(); 181 | setInt64(sp + 8, msec / 1000); 182 | this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true); 183 | }, 184 | 185 | // func scheduleTimeoutEvent(delay int64) int32 186 | "runtime.scheduleTimeoutEvent": (sp) => { 187 | sp >>>= 0; 188 | const id = this._nextCallbackTimeoutID; 189 | this._nextCallbackTimeoutID++; 190 | this._scheduledTimeouts.set( 191 | id, 192 | setTimeout(() => { 193 | this._resume(); 194 | while (this._scheduledTimeouts.has(id)) { 195 | // for some reason Go failed to register the timeout event, log and try again 196 | // (temporary workaround for https://github.com/golang/go/issues/28975) 197 | console.warn("scheduleTimeoutEvent: missed timeout event"); 198 | this._resume(); 199 | } 200 | }, getInt64(sp + 8)) 201 | ); 202 | this.mem.setInt32(sp + 16, id, true); 203 | }, 204 | 205 | // func clearTimeoutEvent(id int32) 206 | "runtime.clearTimeoutEvent": (sp) => { 207 | sp >>>= 0; 208 | const id = this.mem.getInt32(sp + 8, true); 209 | clearTimeout(this._scheduledTimeouts.get(id)); 210 | this._scheduledTimeouts.delete(id); 211 | }, 212 | 213 | // func getRandomData(r []byte) 214 | "runtime.getRandomData": (sp) => { 215 | sp >>>= 0; 216 | crypto.getRandomValues(loadSlice(sp + 8)); 217 | }, 218 | 219 | // func finalizeRef(v ref) 220 | "syscall/js.finalizeRef": (sp) => { 221 | sp >>>= 0; 222 | const id = this.mem.getUint32(sp + 8, true); 223 | this._goRefCounts[id]--; 224 | if (this._goRefCounts[id] === 0) { 225 | const v = this._values[id]; 226 | this._values[id] = null; 227 | this._ids.delete(v); 228 | this._idPool.push(id); 229 | } 230 | }, 231 | 232 | // func stringVal(value string) ref 233 | "syscall/js.stringVal": (sp) => { 234 | sp >>>= 0; 235 | storeValue(sp + 24, loadString(sp + 8)); 236 | }, 237 | 238 | // func valueGet(v ref, p string) ref 239 | "syscall/js.valueGet": (sp) => { 240 | sp >>>= 0; 241 | const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); 242 | sp = this._inst.exports.getsp() >>> 0; // see comment above 243 | storeValue(sp + 32, result); 244 | }, 245 | 246 | // func valueSet(v ref, p string, x ref) 247 | "syscall/js.valueSet": (sp) => { 248 | sp >>>= 0; 249 | Reflect.set( 250 | loadValue(sp + 8), 251 | loadString(sp + 16), 252 | loadValue(sp + 32) 253 | ); 254 | }, 255 | 256 | // func valueDelete(v ref, p string) 257 | "syscall/js.valueDelete": (sp) => { 258 | sp >>>= 0; 259 | Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); 260 | }, 261 | 262 | // func valueIndex(v ref, i int) ref 263 | "syscall/js.valueIndex": (sp) => { 264 | sp >>>= 0; 265 | storeValue( 266 | sp + 24, 267 | Reflect.get(loadValue(sp + 8), getInt64(sp + 16)) 268 | ); 269 | }, 270 | 271 | // valueSetIndex(v ref, i int, x ref) 272 | "syscall/js.valueSetIndex": (sp) => { 273 | sp >>>= 0; 274 | Reflect.set( 275 | loadValue(sp + 8), 276 | getInt64(sp + 16), 277 | loadValue(sp + 24) 278 | ); 279 | }, 280 | 281 | // func valueCall(v ref, m string, args []ref) (ref, bool) 282 | "syscall/js.valueCall": (sp) => { 283 | sp >>>= 0; 284 | try { 285 | const v = loadValue(sp + 8); 286 | const m = Reflect.get(v, loadString(sp + 16)); 287 | const args = loadSliceOfValues(sp + 32); 288 | const result = Reflect.apply(m, v, args); 289 | sp = this._inst.exports.getsp() >>> 0; // see comment above 290 | storeValue(sp + 56, result); 291 | this.mem.setUint8(sp + 64, 1); 292 | } catch (err) { 293 | sp = this._inst.exports.getsp() >>> 0; // see comment above 294 | storeValue(sp + 56, err); 295 | this.mem.setUint8(sp + 64, 0); 296 | } 297 | }, 298 | 299 | // func valueInvoke(v ref, args []ref) (ref, bool) 300 | "syscall/js.valueInvoke": (sp) => { 301 | sp >>>= 0; 302 | try { 303 | const v = loadValue(sp + 8); 304 | const args = loadSliceOfValues(sp + 16); 305 | const result = Reflect.apply(v, undefined, args); 306 | sp = this._inst.exports.getsp() >>> 0; // see comment above 307 | storeValue(sp + 40, result); 308 | this.mem.setUint8(sp + 48, 1); 309 | } catch (err) { 310 | sp = this._inst.exports.getsp() >>> 0; // see comment above 311 | storeValue(sp + 40, err); 312 | this.mem.setUint8(sp + 48, 0); 313 | } 314 | }, 315 | 316 | // func valueNew(v ref, args []ref) (ref, bool) 317 | "syscall/js.valueNew": (sp) => { 318 | sp >>>= 0; 319 | try { 320 | const v = loadValue(sp + 8); 321 | const args = loadSliceOfValues(sp + 16); 322 | const result = Reflect.construct(v, args); 323 | sp = this._inst.exports.getsp() >>> 0; // see comment above 324 | storeValue(sp + 40, result); 325 | this.mem.setUint8(sp + 48, 1); 326 | } catch (err) { 327 | sp = this._inst.exports.getsp() >>> 0; // see comment above 328 | storeValue(sp + 40, err); 329 | this.mem.setUint8(sp + 48, 0); 330 | } 331 | }, 332 | 333 | // func valueLength(v ref) int 334 | "syscall/js.valueLength": (sp) => { 335 | sp >>>= 0; 336 | setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); 337 | }, 338 | 339 | // valuePrepareString(v ref) (ref, int) 340 | "syscall/js.valuePrepareString": (sp) => { 341 | sp >>>= 0; 342 | const str = encoder.encode(String(loadValue(sp + 8))); 343 | storeValue(sp + 16, str); 344 | setInt64(sp + 24, str.length); 345 | }, 346 | 347 | // valueLoadString(v ref, b []byte) 348 | "syscall/js.valueLoadString": (sp) => { 349 | sp >>>= 0; 350 | const str = loadValue(sp + 8); 351 | loadSlice(sp + 16).set(str); 352 | }, 353 | 354 | // func valueInstanceOf(v ref, t ref) bool 355 | "syscall/js.valueInstanceOf": (sp) => { 356 | sp >>>= 0; 357 | this.mem.setUint8( 358 | sp + 24, 359 | loadValue(sp + 8) instanceof loadValue(sp + 16) ? 1 : 0 360 | ); 361 | }, 362 | 363 | // func copyBytesToGo(dst []byte, src ref) (int, bool) 364 | "syscall/js.copyBytesToGo": (sp) => { 365 | sp >>>= 0; 366 | const dst = loadSlice(sp + 8); 367 | const src = loadValue(sp + 32); 368 | if ( 369 | !(src instanceof Uint8Array || src instanceof Uint8ClampedArray) 370 | ) { 371 | this.mem.setUint8(sp + 48, 0); 372 | return; 373 | } 374 | const toCopy = src.subarray(0, dst.length); 375 | dst.set(toCopy); 376 | setInt64(sp + 40, toCopy.length); 377 | this.mem.setUint8(sp + 48, 1); 378 | }, 379 | 380 | // func copyBytesToJS(dst ref, src []byte) (int, bool) 381 | "syscall/js.copyBytesToJS": (sp) => { 382 | sp >>>= 0; 383 | const dst = loadValue(sp + 8); 384 | const src = loadSlice(sp + 16); 385 | if ( 386 | !(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray) 387 | ) { 388 | this.mem.setUint8(sp + 48, 0); 389 | return; 390 | } 391 | const toCopy = src.subarray(0, dst.length); 392 | dst.set(toCopy); 393 | setInt64(sp + 40, toCopy.length); 394 | this.mem.setUint8(sp + 48, 1); 395 | }, 396 | 397 | debug: (value) => { 398 | console.log(value); 399 | }, 400 | }, 401 | }; 402 | } 403 | 404 | async run(instance, context) { 405 | if (!(instance instanceof WebAssembly.Instance)) { 406 | throw new Error("Go.run: WebAssembly.Instance expected"); 407 | } 408 | this._inst = instance; 409 | this.mem = new DataView(this._inst.exports.mem.buffer); 410 | const globalProxy = new Proxy(globalThis, { 411 | get(target, prop) { 412 | if (prop === "context") { 413 | return context; 414 | } 415 | return Reflect.get(target, prop, target); 416 | }, 417 | }); 418 | this._values = [ 419 | // JS values that Go currently has references to, indexed by reference id 420 | NaN, 421 | 0, 422 | null, 423 | true, 424 | false, 425 | globalProxy, 426 | this, 427 | ]; 428 | this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id 429 | this._ids = new Map([ 430 | // mapping from JS values to reference ids 431 | [0, 1], 432 | [null, 2], 433 | [true, 3], 434 | [false, 4], 435 | [globalProxy, 5], 436 | [this, 6], 437 | ]); 438 | this._idPool = []; // unused ids that have been garbage collected 439 | this.exited = false; // whether the Go program has exited 440 | 441 | // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. 442 | let offset = 4096; 443 | 444 | const strPtr = (str) => { 445 | const ptr = offset; 446 | const bytes = encoder.encode(str + "\0"); 447 | new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); 448 | offset += bytes.length; 449 | if (offset % 8 !== 0) { 450 | offset += 8 - (offset % 8); 451 | } 452 | return ptr; 453 | }; 454 | 455 | const argc = this.argv.length; 456 | 457 | const argvPtrs = []; 458 | this.argv.forEach((arg) => { 459 | argvPtrs.push(strPtr(arg)); 460 | }); 461 | argvPtrs.push(0); 462 | 463 | const keys = Object.keys(this.env).sort(); 464 | keys.forEach((key) => { 465 | argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); 466 | }); 467 | argvPtrs.push(0); 468 | 469 | const argv = offset; 470 | argvPtrs.forEach((ptr) => { 471 | this.mem.setUint32(offset, ptr, true); 472 | this.mem.setUint32(offset + 4, 0, true); 473 | offset += 8; 474 | }); 475 | 476 | // The linker guarantees global data starts from at least wasmMinDataAddr. 477 | // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr. 478 | const wasmMinDataAddr = 4096 + 8192; 479 | if (offset >= wasmMinDataAddr) { 480 | throw new Error( 481 | "total length of command line and environment variables exceeds limit" 482 | ); 483 | } 484 | 485 | this._inst.exports.run(argc, argv); 486 | if (this.exited) { 487 | this._resolveExitPromise(); 488 | } 489 | await this._exitPromise; 490 | } 491 | 492 | _resume() { 493 | if (this.exited) { 494 | throw new Error("Go program has already exited"); 495 | } 496 | this._inst.exports.resume(); 497 | if (this.exited) { 498 | this._resolveExitPromise(); 499 | } 500 | } 501 | 502 | _makeFuncWrapper(id) { 503 | const go = this; 504 | return function () { 505 | const event = { id: id, this: this, args: arguments }; 506 | go._pendingEvent = event; 507 | go._resume(); 508 | return event.result; 509 | }; 510 | } 511 | }; 512 | })(); 513 | -------------------------------------------------------------------------------- /src/wasm_exec_tinygo.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | // 5 | // This file has been modified for use by the TinyGo compiler. 6 | 7 | (() => { 8 | // Map multiple JavaScript environments to a single common API, 9 | // preferring web standards over Node.js API. 10 | // 11 | // Environments considered: 12 | // - Browsers 13 | // - Node.js 14 | // - Electron 15 | // - Parcel 16 | 17 | const encoder = new TextEncoder("utf-8"); 18 | const decoder = new TextDecoder("utf-8"); 19 | let reinterpretBuf = new DataView(new ArrayBuffer(8)); 20 | var logLine = []; 21 | 22 | globalThis.TinyGo = class { 23 | constructor() { 24 | this._callbackTimeouts = new Map(); 25 | this._nextCallbackTimeoutID = 1; 26 | 27 | const mem = () => { 28 | // The buffer may change when requesting more memory. 29 | return new DataView(this._inst.exports.memory.buffer); 30 | }; 31 | 32 | const unboxValue = (v_ref) => { 33 | reinterpretBuf.setBigInt64(0, v_ref, true); 34 | const f = reinterpretBuf.getFloat64(0, true); 35 | if (f === 0) { 36 | return undefined; 37 | } 38 | if (!isNaN(f)) { 39 | return f; 40 | } 41 | 42 | const id = v_ref & 0xffffffffn; 43 | return this._values[id]; 44 | }; 45 | 46 | const loadValue = (addr) => { 47 | let v_ref = mem().getBigUint64(addr, true); 48 | return unboxValue(v_ref); 49 | }; 50 | 51 | const boxValue = (v) => { 52 | const nanHead = 0x7ff80000n; 53 | 54 | if (typeof v === "number") { 55 | if (isNaN(v)) { 56 | return nanHead << 32n; 57 | } 58 | if (v === 0) { 59 | return (nanHead << 32n) | 1n; 60 | } 61 | reinterpretBuf.setFloat64(0, v, true); 62 | return reinterpretBuf.getBigInt64(0, true); 63 | } 64 | 65 | switch (v) { 66 | case undefined: 67 | return 0n; 68 | case null: 69 | return (nanHead << 32n) | 2n; 70 | case true: 71 | return (nanHead << 32n) | 3n; 72 | case false: 73 | return (nanHead << 32n) | 4n; 74 | } 75 | 76 | let id = this._ids.get(v); 77 | if (id === undefined) { 78 | id = this._idPool.pop(); 79 | if (id === undefined) { 80 | id = BigInt(this._values.length); 81 | } 82 | this._values[id] = v; 83 | this._goRefCounts[id] = 0; 84 | this._ids.set(v, id); 85 | } 86 | this._goRefCounts[id]++; 87 | let typeFlag = 1n; 88 | switch (typeof v) { 89 | case "string": 90 | typeFlag = 2n; 91 | break; 92 | case "symbol": 93 | typeFlag = 3n; 94 | break; 95 | case "function": 96 | typeFlag = 4n; 97 | break; 98 | } 99 | return id | ((nanHead | typeFlag) << 32n); 100 | }; 101 | 102 | const storeValue = (addr, v) => { 103 | let v_ref = boxValue(v); 104 | mem().setBigUint64(addr, v_ref, true); 105 | }; 106 | 107 | const loadSlice = (array, len, cap) => { 108 | return new Uint8Array(this._inst.exports.memory.buffer, array, len); 109 | }; 110 | 111 | const loadSliceOfValues = (array, len, cap) => { 112 | const a = new Array(len); 113 | for (let i = 0; i < len; i++) { 114 | a[i] = loadValue(array + i * 8); 115 | } 116 | return a; 117 | }; 118 | 119 | const loadString = (ptr, len) => { 120 | return decoder.decode( 121 | new DataView(this._inst.exports.memory.buffer, ptr, len) 122 | ); 123 | }; 124 | 125 | const timeOrigin = Date.now() - performance.now(); 126 | this.importObject = { 127 | wasi_snapshot_preview1: { 128 | // https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_write 129 | fd_write: function (fd, iovs_ptr, iovs_len, nwritten_ptr) { 130 | let nwritten = 0; 131 | if (fd == 1) { 132 | for (let iovs_i = 0; iovs_i < iovs_len; iovs_i++) { 133 | let iov_ptr = iovs_ptr + iovs_i * 8; // assuming wasm32 134 | let ptr = mem().getUint32(iov_ptr + 0, true); 135 | let len = mem().getUint32(iov_ptr + 4, true); 136 | nwritten += len; 137 | for (let i = 0; i < len; i++) { 138 | let c = mem().getUint8(ptr + i); 139 | if (c == 13) { 140 | // CR 141 | // ignore 142 | } else if (c == 10) { 143 | // LF 144 | // write line 145 | let line = decoder.decode(new Uint8Array(logLine)); 146 | logLine = []; 147 | console.log(line); 148 | } else { 149 | logLine.push(c); 150 | } 151 | } 152 | } 153 | } else { 154 | console.error("invalid file descriptor:", fd); 155 | } 156 | mem().setUint32(nwritten_ptr, nwritten, true); 157 | return 0; 158 | }, 159 | fd_close: () => 0, // dummy 160 | fd_fdstat_get: () => 0, // dummy 161 | fd_seek: () => 0, // dummy 162 | proc_exit: (code) => { 163 | if (globalThis.process) { 164 | // Node.js 165 | process.exit(code); 166 | } else { 167 | // Can't exit in a browser. 168 | throw "trying to exit with code " + code; 169 | } 170 | }, 171 | random_get: (bufPtr, bufLen) => { 172 | crypto.getRandomValues(loadSlice(bufPtr, bufLen)); 173 | return 0; 174 | }, 175 | }, 176 | gojs: { 177 | // func ticks() float64 178 | "runtime.ticks": () => { 179 | return timeOrigin + performance.now(); 180 | }, 181 | 182 | // func sleepTicks(timeout float64) 183 | "runtime.sleepTicks": (timeout) => { 184 | // Do not sleep, only reactivate scheduler after the given timeout. 185 | setTimeout(this._inst.exports.go_scheduler, timeout); 186 | }, 187 | 188 | // func finalizeRef(v ref) 189 | "syscall/js.finalizeRef": (v_ref) => { 190 | // Note: TinyGo does not support finalizers so this should never be 191 | // called. 192 | // console.error('syscall/js.finalizeRef not implemented'); 193 | }, 194 | 195 | // func stringVal(value string) ref 196 | "syscall/js.stringVal": (value_ptr, value_len) => { 197 | const s = loadString(value_ptr, value_len); 198 | return boxValue(s); 199 | }, 200 | 201 | // func valueGet(v ref, p string) ref 202 | "syscall/js.valueGet": (v_ref, p_ptr, p_len) => { 203 | let prop = loadString(p_ptr, p_len); 204 | let v = unboxValue(v_ref); 205 | let result = Reflect.get(v, prop); 206 | return boxValue(result); 207 | }, 208 | 209 | // func valueSet(v ref, p string, x ref) 210 | "syscall/js.valueSet": (v_ref, p_ptr, p_len, x_ref) => { 211 | const v = unboxValue(v_ref); 212 | const p = loadString(p_ptr, p_len); 213 | const x = unboxValue(x_ref); 214 | Reflect.set(v, p, x); 215 | }, 216 | 217 | // func valueDelete(v ref, p string) 218 | "syscall/js.valueDelete": (v_ref, p_ptr, p_len) => { 219 | const v = unboxValue(v_ref); 220 | const p = loadString(p_ptr, p_len); 221 | Reflect.deleteProperty(v, p); 222 | }, 223 | 224 | // func valueIndex(v ref, i int) ref 225 | "syscall/js.valueIndex": (v_ref, i) => { 226 | return boxValue(Reflect.get(unboxValue(v_ref), i)); 227 | }, 228 | 229 | // valueSetIndex(v ref, i int, x ref) 230 | "syscall/js.valueSetIndex": (v_ref, i, x_ref) => { 231 | Reflect.set(unboxValue(v_ref), i, unboxValue(x_ref)); 232 | }, 233 | 234 | // func valueCall(v ref, m string, args []ref) (ref, bool) 235 | "syscall/js.valueCall": ( 236 | ret_addr, 237 | v_ref, 238 | m_ptr, 239 | m_len, 240 | args_ptr, 241 | args_len, 242 | args_cap 243 | ) => { 244 | const v = unboxValue(v_ref); 245 | const name = loadString(m_ptr, m_len); 246 | const args = loadSliceOfValues(args_ptr, args_len, args_cap); 247 | try { 248 | const m = Reflect.get(v, name); 249 | storeValue(ret_addr, Reflect.apply(m, v, args)); 250 | mem().setUint8(ret_addr + 8, 1); 251 | } catch (err) { 252 | storeValue(ret_addr, err); 253 | mem().setUint8(ret_addr + 8, 0); 254 | } 255 | }, 256 | 257 | // func valueInvoke(v ref, args []ref) (ref, bool) 258 | "syscall/js.valueInvoke": ( 259 | ret_addr, 260 | v_ref, 261 | args_ptr, 262 | args_len, 263 | args_cap 264 | ) => { 265 | try { 266 | const v = unboxValue(v_ref); 267 | const args = loadSliceOfValues(args_ptr, args_len, args_cap); 268 | storeValue(ret_addr, Reflect.apply(v, undefined, args)); 269 | mem().setUint8(ret_addr + 8, 1); 270 | } catch (err) { 271 | storeValue(ret_addr, err); 272 | mem().setUint8(ret_addr + 8, 0); 273 | } 274 | }, 275 | 276 | // func valueNew(v ref, args []ref) (ref, bool) 277 | "syscall/js.valueNew": ( 278 | ret_addr, 279 | v_ref, 280 | args_ptr, 281 | args_len, 282 | args_cap 283 | ) => { 284 | const v = unboxValue(v_ref); 285 | const args = loadSliceOfValues(args_ptr, args_len, args_cap); 286 | try { 287 | storeValue(ret_addr, Reflect.construct(v, args)); 288 | mem().setUint8(ret_addr + 8, 1); 289 | } catch (err) { 290 | storeValue(ret_addr, err); 291 | mem().setUint8(ret_addr + 8, 0); 292 | } 293 | }, 294 | 295 | // func valueLength(v ref) int 296 | "syscall/js.valueLength": (v_ref) => { 297 | return unboxValue(v_ref).length; 298 | }, 299 | 300 | // valuePrepareString(v ref) (ref, int) 301 | "syscall/js.valuePrepareString": (ret_addr, v_ref) => { 302 | const s = String(unboxValue(v_ref)); 303 | const str = encoder.encode(s); 304 | storeValue(ret_addr, str); 305 | mem().setInt32(ret_addr + 8, str.length, true); 306 | }, 307 | 308 | // valueLoadString(v ref, b []byte) 309 | "syscall/js.valueLoadString": ( 310 | v_ref, 311 | slice_ptr, 312 | slice_len, 313 | slice_cap 314 | ) => { 315 | const str = unboxValue(v_ref); 316 | loadSlice(slice_ptr, slice_len, slice_cap).set(str); 317 | }, 318 | 319 | // func valueInstanceOf(v ref, t ref) bool 320 | "syscall/js.valueInstanceOf": (v_ref, t_ref) => { 321 | return unboxValue(v_ref) instanceof unboxValue(t_ref); 322 | }, 323 | 324 | // func copyBytesToGo(dst []byte, src ref) (int, bool) 325 | "syscall/js.copyBytesToGo": ( 326 | ret_addr, 327 | dest_addr, 328 | dest_len, 329 | dest_cap, 330 | src_ref 331 | ) => { 332 | let num_bytes_copied_addr = ret_addr; 333 | let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable 334 | 335 | const dst = loadSlice(dest_addr, dest_len); 336 | const src = unboxValue(src_ref); 337 | if ( 338 | !(src instanceof Uint8Array || src instanceof Uint8ClampedArray) 339 | ) { 340 | mem().setUint8(returned_status_addr, 0); // Return "not ok" status 341 | return; 342 | } 343 | const toCopy = src.subarray(0, dst.length); 344 | dst.set(toCopy); 345 | mem().setUint32(num_bytes_copied_addr, toCopy.length, true); 346 | mem().setUint8(returned_status_addr, 1); // Return "ok" status 347 | }, 348 | 349 | // copyBytesToJS(dst ref, src []byte) (int, bool) 350 | // Originally copied from upstream Go project, then modified: 351 | // https://github.com/golang/go/blob/3f995c3f3b43033013013e6c7ccc93a9b1411ca9/misc/wasm/wasm_exec.js#L404-L416 352 | "syscall/js.copyBytesToJS": ( 353 | ret_addr, 354 | dst_ref, 355 | src_addr, 356 | src_len, 357 | src_cap 358 | ) => { 359 | let num_bytes_copied_addr = ret_addr; 360 | let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable 361 | 362 | const dst = unboxValue(dst_ref); 363 | const src = loadSlice(src_addr, src_len); 364 | if ( 365 | !(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray) 366 | ) { 367 | mem().setUint8(returned_status_addr, 0); // Return "not ok" status 368 | return; 369 | } 370 | const toCopy = src.subarray(0, dst.length); 371 | dst.set(toCopy); 372 | mem().setUint32(num_bytes_copied_addr, toCopy.length, true); 373 | mem().setUint8(returned_status_addr, 1); // Return "ok" status 374 | }, 375 | }, 376 | }; 377 | 378 | // Go 1.20 uses 'env'. Go 1.21 uses 'gojs'. 379 | // For compatibility, we use both as long as Go 1.20 is supported. 380 | this.importObject.env = this.importObject.gojs; 381 | } 382 | 383 | async run(instance, context) { 384 | this._inst = instance; 385 | const globalProxy = new Proxy(globalThis, { 386 | get(target, prop) { 387 | if (prop === "context") { 388 | return context; 389 | } 390 | return Reflect.get(target, prop, target); 391 | }, 392 | }); 393 | this._values = [ 394 | // JS values that Go currently has references to, indexed by reference id 395 | NaN, 396 | 0, 397 | null, 398 | true, 399 | false, 400 | globalProxy, 401 | this, 402 | ]; 403 | this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id 404 | this._ids = new Map(); // mapping from JS values to reference ids 405 | this._idPool = []; // unused ids that have been garbage collected 406 | this.exited = false; // whether the Go program has exited 407 | 408 | const mem = new DataView(this._inst.exports.memory.buffer); 409 | 410 | while (true) { 411 | const callbackPromise = new Promise((resolve) => { 412 | this._resolveCallbackPromise = () => { 413 | if (this.exited) { 414 | throw new Error("bad callback: Go program has already exited"); 415 | } 416 | setTimeout(resolve, 0); // make sure it is asynchronous 417 | }; 418 | }); 419 | this._inst.exports._start(); 420 | if (this.exited) { 421 | break; 422 | } 423 | await callbackPromise; 424 | } 425 | } 426 | 427 | _resume() { 428 | if (this.exited) { 429 | throw new Error("Go program has already exited"); 430 | } 431 | this._inst.exports.resume(); 432 | if (this.exited) { 433 | this._resolveExitPromise(); 434 | } 435 | } 436 | 437 | _makeFuncWrapper(id) { 438 | const go = this; 439 | return function () { 440 | const event = { id: id, this: this, args: arguments }; 441 | go._pendingEvent = event; 442 | go._resume(); 443 | return event.result; 444 | }; 445 | } 446 | }; 447 | })(); 448 | -------------------------------------------------------------------------------- /src/workers-go.ts: -------------------------------------------------------------------------------- 1 | import { Context, Env } from "hono"; 2 | import { connect } from "cloudflare:sockets"; 3 | import "./wasm_exec_common.js"; 4 | import "./wasm_exec_go.js"; 5 | import "./wasm_exec_tinygo.js"; 6 | 7 | declare const Go: any; 8 | declare const TinyGo: any; 9 | 10 | Object.defineProperty(globalThis, "tryCatch", { 11 | value: (fn: () => void) => { 12 | try { 13 | return { 14 | result: fn(), 15 | }; 16 | } catch (e) { 17 | return { 18 | error: e, 19 | }; 20 | } 21 | }, 22 | writable: false, 23 | }); 24 | 25 | export function createRuntimeContext( 26 | env: E, 27 | ctx: Context, 28 | binding: object 29 | ) { 30 | return { 31 | env, 32 | ctx, 33 | connect, 34 | binding, 35 | }; 36 | } 37 | 38 | export async function run( 39 | ctx: ReturnType, 40 | mod: WebAssembly.Module, 41 | isTinyGo = false 42 | ) { 43 | const go = isTinyGo ? new TinyGo() : new Go(); 44 | 45 | let ready: (v?: unknown) => void; 46 | const readyPromise = new Promise((resolve) => { 47 | ready = resolve; 48 | }); 49 | const instance = new WebAssembly.Instance(mod, { 50 | ...go.importObject, 51 | workers: { 52 | ready: () => { 53 | ready(); 54 | }, 55 | }, 56 | }); 57 | go.run(instance, ctx); 58 | await readyPromise; 59 | } 60 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "moduleResolution": "Node", 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "skipLibCheck": true, 11 | "noUnusedLocals": false, 12 | "noUnusedParameters": true, 13 | "types": [ 14 | "node", 15 | "@cloudflare/workers-types" 16 | ], 17 | "rootDir": "./src", 18 | "outDir": "./dist", 19 | }, 20 | "include": [ 21 | "src/**/*", 22 | ], 23 | } -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "hono-middleware-go" 2 | compatibility_date = "2023-12-01" 3 | 4 | # [vars] 5 | # MY_VARIABLE = "production_value" 6 | 7 | # [[kv_namespaces]] 8 | # binding = "MY_KV_NAMESPACE" 9 | # id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 10 | 11 | # [[r2_buckets]] 12 | # binding = "MY_BUCKET" 13 | # bucket_name = "my-bucket" 14 | 15 | # [[d1_databases]] 16 | # binding = "DB" 17 | # database_name = "my-database" 18 | # database_id = "" 19 | --------------------------------------------------------------------------------