Changes
13 changed files (+291/-165)
-
-
@@ -6,6 +6,7 @@ # SPDX-License-Identifier: AGPL-3.0-onlydprint 0.47.5 bun 1.1.45 terraform 1.10.3 protoc-gen-connect-go 1.18.0 protoc-gen-go 1.36.5 go 1.24.1 zig 0.14.0
-
-
-
@@ -1,6 +1,13 @@github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
-
-
-
@@ -10,3 +10,7 @@# What: ビルドされた .js と .d.ts ファイル。 # Why: 編集するものではないため。 /lib # What: Go 製サーバの実行ファイル。 # Why: バイナリ。 /backend
-
-
-
@@ -5,4 +5,11 @@ module pocka.jp/x/yamori/backendgo 1.24.1 require github.com/tetratelabs/wazero v1.9.0 require ( connectrpc.com/connect v1.18.1 github.com/tetratelabs/wazero v1.9.0 golang.org/x/net v0.23.0 google.golang.org/protobuf v1.36.5 ) require golang.org/x/text v0.14.0 // indirect
-
-
-
@@ -1,1 +1,8 @@connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
-
-
-
@@ -23,8 +23,15 @@ "output": ["lib/**"],"dependencies": ["tsconfig"], "packageLocks": ["bun.lockb"] }, "make:bin": { "command": "go build", "files": ["**/*.go", "go.mod", "go.sum"], "output": ["backend"], "dependencies": ["wasm"], "packageLocks": [] }, "make": { "dependencies": ["make:wasm", "make:js"] "dependencies": ["make:wasm", "make:js", "make:bin"] }, "check": { "command": "tsc",
-
@@ -50,6 +57,15 @@ "files": ["zig-out/bin/*.wasm"],"dependencies": [ { "script": "make:wasm", "cascade": false } ] }, "bin": { "files": ["backend"], "dependencies": [ { "script": "make:bin", "cascade": false } ]
-
-
-
@@ -4,16 +4,13 @@package main import ( "bytes" "context" _ "embed" "fmt" "log" "net/http" "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/api" _ "pocka.jp/x/yamori/proto/go/meta/v1" "pocka.jp/x/yamori/backend/src" "pocka.jp/x/yamori/backend/src/connect_go" ) //go:embed zig-out/bin/yamori_backend.wasm
-
@@ -22,161 +19,12 @@func main() { ctx := context.Background() core, err := initCore(ctx) core, err := src.Init(ctx, coreWasm) if err != nil { log.Panicln(err) } defer core.Close(ctx) resp, err := core.Handle(ctx, "yamori.meta.v1.MetaService", "Ping", []byte{}) if err != nil { log.Panicln(err) } log.Printf("%v\n", resp) } // Core は WASM で実装されており、特にメモリアクセスでかなり煩雑になる。 // この構造体はそれらの操作を抽象化する。 // 内部実装は基本的に JavaScript 実装 (worker.ts) のポートとなっている。 type Core struct { wasmRuntime wazero.Runtime mod api.Module corePtr uint64 } func initCore(ctx context.Context) (*Core, error) { runtime := wazero.NewRuntime(ctx) mod, err := runtime.Instantiate(ctx, coreWasm) if err != nil { return nil, err } initCore := mod.ExportedFunction("init_core") initCoreResults, err := initCore.Call(ctx) if err != nil { return nil, err } return &Core{ wasmRuntime: runtime, mod: mod, corePtr: initCoreResults[0], }, nil } // allocate は WASM の "allocate_bytes" を呼び、確保されたアドレスを返す。 func (core Core) allocate(ctx context.Context, length uint32) (uint32, error) { fn := core.mod.ExportedFunction("allocate_bytes") results, err := fn.Call(ctx, uint64(length)) if err != nil { return 0, err } return uint32(results[0]), nil } // copy は受け取ったバイト列を WASM のメモリ上にコピーして WASM から読めるようにする。 // 確保したアドレスとバイト長を返す。 func (core Core) copy(ctx context.Context, bytes []byte) (uint32, uint32, error) { length := uint32(len(bytes)) ptr, err := core.allocate(ctx, length) if err != nil { return 0, 0, err } if length == 0 { return ptr, length, nil } if !core.mod.Memory().Write(ptr, bytes) { return 0, 0, fmt.Errorf("Failed to write to memory (%d bytes)", length) } return ptr, length, nil } // Handle は RPC のリクエストを受けてレスポンスを返す関数。 // サービス名もメソッド名も変更しないため、レスポンスのバイナリデータのみを返す。 func (core Core) Handle(ctx context.Context, service string, method string, data []byte) ([]byte, error) { freeBytes := core.mod.ExportedFunction("free_bytes") servicePtr, serviceLen, err := core.copy(ctx, []byte(service)) if err != nil { return nil, err } defer freeBytes.Call(ctx, uint64(servicePtr), uint64(serviceLen)) methodPtr, methodLen, err := core.copy(ctx, []byte(method)) if err != nil { return nil, err } defer freeBytes.Call(ctx, uint64(methodPtr), uint64(methodLen)) dataPtr, dataLen, err := core.copy(ctx, data) if err != nil { return nil, err } defer freeBytes.Call(ctx, uint64(dataPtr), uint64(dataLen)) destroyResponse := core.mod.ExportedFunction("destroy_response") createResponse := core.mod.ExportedFunction("create_response") createResponseResult, err := createResponse.Call(ctx) if err != nil { return nil, err } defer destroyResponse.Call(ctx, createResponseResult[0]) handleFn := core.mod.ExportedFunction("handle") handleResults, err := handleFn.Call( ctx, core.corePtr, uint64(servicePtr), uint64(serviceLen), uint64(methodPtr), uint64(methodLen), uint64(dataPtr), uint64(dataLen), createResponseResult[0], ) if err != nil { return nil, err } if handleResults[0] != 0 { return nil, fmt.Errorf("handler returned non-zero result: %d", handleResults[0]) } getResponsePtr := core.mod.ExportedFunction("get_response_ptr") getResponseLen := core.mod.ExportedFunction("get_response_len") responsePtr, err := getResponsePtr.Call(ctx, createResponseResult[0]) if err != nil { return nil, err } responseLen, err := getResponseLen.Call(ctx, createResponseResult[0]) if err != nil { return nil, err } if responseLen[0] == 0 { return []byte{}, nil } responseData, isRangeOK := core.mod.Memory().Read(uint32(responsePtr[0]), uint32(responseLen[0])) if !isRangeOK { return nil, fmt.Errorf("get_response_ptr/len returned out-of-range address or length") } // destroy_response によってメモリが解放されるため、返す前にクローンする // 必要がある。しないと read after free になってしまう。 return bytes.Clone(responseData), nil } // Close を呼ぶと以降は Core を使った操作はできなくなる。 // WASM ランタイムのリソースが解放される。 func (core *Core) Close(ctx context.Context) { core.mod.Close(ctx) core.wasmRuntime.Close(ctx) core = nil handler := connect_go.InitHandler(core) http.ListenAndServe("localhost:8765", handler) }
-
-
-
@@ -0,0 +1,21 @@// SPDX-FileCopyrightText: 2025 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only package connect_go import ( "net/http" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" "pocka.jp/x/yamori/backend/src" ) func InitHandler(core *src.Core) http.Handler { mux := http.NewServeMux() meta := &metaServer{core: core} meta.register(mux) return h2c.NewHandler(mux, &http2.Server{}) }
-
-
-
@@ -0,0 +1,48 @@// SPDX-FileCopyrightText: 2025 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only package connect_go import ( "context" "net/http" "connectrpc.com/connect" "google.golang.org/protobuf/proto" "pocka.jp/x/yamori/backend/src" metaV1 "pocka.jp/x/yamori/proto/go/meta/v1" metaV1connect "pocka.jp/x/yamori/proto/go/meta/v1/v1connect" ) type metaServer struct { core *src.Core } func (s *metaServer) Ping( ctx context.Context, req *connect.Request[metaV1.PingRequest], ) (*connect.Response[metaV1.PingResponse], error) { reqBinary, err := proto.Marshal(req.Msg) if err != nil { return nil, err } data, err := s.core.Handle(ctx, "yamori.meta.v1.MetaService", "Ping", reqBinary) if err != nil { return nil, err } res := metaV1.PingResponse{} if err := proto.Unmarshal(data, &res); err != nil { return nil, err } return connect.NewResponse(&res), nil } func (s *metaServer) register(mux *http.ServeMux) { path, handler := metaV1connect.NewMetaServiceHandler(s) mux.Handle(path, handler) }
-
-
-
@@ -0,0 +1,158 @@// SPDX-FileCopyrightText: 2025 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only package src import ( "bytes" "context" "fmt" "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/api" ) // Core は WASM で実装されており、特にメモリアクセスでかなり煩雑になる。 // この構造体はそれらの操作を抽象化する。 // 内部実装は基本的に JavaScript 実装 (worker.ts) のポートとなっている。 type Core struct { wasmRuntime wazero.Runtime mod api.Module corePtr uint64 } func Init(ctx context.Context, wasm []byte) (*Core, error) { runtime := wazero.NewRuntime(ctx) mod, err := runtime.Instantiate(ctx, wasm) if err != nil { return nil, err } initCore := mod.ExportedFunction("init_core") initCoreResults, err := initCore.Call(ctx) if err != nil { return nil, err } return &Core{ wasmRuntime: runtime, mod: mod, corePtr: initCoreResults[0], }, nil } // allocate は WASM の "allocate_bytes" を呼び、確保されたアドレスを返す。 func (core Core) allocate(ctx context.Context, length uint32) (uint32, error) { fn := core.mod.ExportedFunction("allocate_bytes") results, err := fn.Call(ctx, uint64(length)) if err != nil { return 0, err } return uint32(results[0]), nil } // copy は受け取ったバイト列を WASM のメモリ上にコピーして WASM から読めるようにする。 // 確保したアドレスとバイト長を返す。 func (core Core) copy(ctx context.Context, bytes []byte) (uint32, uint32, error) { length := uint32(len(bytes)) ptr, err := core.allocate(ctx, length) if err != nil { return 0, 0, err } if length == 0 { return ptr, length, nil } if !core.mod.Memory().Write(ptr, bytes) { return 0, 0, fmt.Errorf("Failed to write to memory (%d bytes)", length) } return ptr, length, nil } // Handle は RPC のリクエストを受けてレスポンスを返す関数。 // サービス名もメソッド名も変更しないため、レスポンスのバイナリデータのみを返す。 func (core Core) Handle(ctx context.Context, service string, method string, data []byte) ([]byte, error) { freeBytes := core.mod.ExportedFunction("free_bytes") servicePtr, serviceLen, err := core.copy(ctx, []byte(service)) if err != nil { return nil, err } defer freeBytes.Call(ctx, uint64(servicePtr), uint64(serviceLen)) methodPtr, methodLen, err := core.copy(ctx, []byte(method)) if err != nil { return nil, err } defer freeBytes.Call(ctx, uint64(methodPtr), uint64(methodLen)) dataPtr, dataLen, err := core.copy(ctx, data) if err != nil { return nil, err } defer freeBytes.Call(ctx, uint64(dataPtr), uint64(dataLen)) destroyResponse := core.mod.ExportedFunction("destroy_response") createResponse := core.mod.ExportedFunction("create_response") createResponseResult, err := createResponse.Call(ctx) if err != nil { return nil, err } defer destroyResponse.Call(ctx, createResponseResult[0]) handleFn := core.mod.ExportedFunction("handle") handleResults, err := handleFn.Call( ctx, core.corePtr, uint64(servicePtr), uint64(serviceLen), uint64(methodPtr), uint64(methodLen), uint64(dataPtr), uint64(dataLen), createResponseResult[0], ) if err != nil { return nil, err } if handleResults[0] != 0 { return nil, fmt.Errorf("handler returned non-zero result: %d", handleResults[0]) } getResponsePtr := core.mod.ExportedFunction("get_response_ptr") getResponseLen := core.mod.ExportedFunction("get_response_len") responsePtr, err := getResponsePtr.Call(ctx, createResponseResult[0]) if err != nil { return nil, err } responseLen, err := getResponseLen.Call(ctx, createResponseResult[0]) if err != nil { return nil, err } if responseLen[0] == 0 { return []byte{}, nil } responseData, isRangeOK := core.mod.Memory().Read(uint32(responsePtr[0]), uint32(responseLen[0])) if !isRangeOK { return nil, fmt.Errorf("get_response_ptr/len returned out-of-range address or length") } // destroy_response によってメモリが解放されるため、返す前にクローンする // 必要がある。しないと read after free になってしまう。 return bytes.Clone(responseData), nil } // Close を呼ぶと以降は Core を使った操作はできなくなる。 // WASM ランタイムのリソースが解放される。 func (core *Core) Close(ctx context.Context) { core.mod.Close(ctx) core.wasmRuntime.Close(ctx) core = nil }
-
-
-
@@ -15,6 +15,10 @@ - local: protoc-gen-goout: go opt: - module=pocka.jp/x/yamori/proto/go - local: protoc-gen-connect-go out: go opt: - module=pocka.jp/x/yamori/proto/go inputs: - directory: .
-
-
-
@@ -5,4 +5,7 @@ module pocka.jp/x/yamori/protogo 1.24.1 require google.golang.org/protobuf v1.36.5 require ( connectrpc.com/connect v1.18.1 google.golang.org/protobuf v1.36.5 )
-
-
-
@@ -1,3 +1,5 @@github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
-