Files
ModelRouter/tools/qoder-wasm-probe/main.go
dev d3bb47bd54 docs(qoder): wasm 接入评估 —— 事实固化 + 可复现取证工具(未实现)
对应主管任务 A/B。**没有实现 wasm 接入**,只交付事实结论与可执行方案。

## A. P1-4 结论固化(docs/channel-repair-plan.md)

原 P1-4 只有一句「阻塞:二进制不在仓库任何位置」,现补成可审计的事实段:

- 绝对路径 / 298606 字节 / sha256 6419471e…55b43d(位于第三方插件
  `dsh-our-free-model` 的 vendor 目录,**不随本仓分发**;本仓
  `git ls-files | grep -i wasm` = 0);
- 它是 wasm-bindgen 产物:31 个 import 全来自单一 JS glue 模块
  `./qoder_auth_wasm_bg.js`,且该 glue **上游也没提供**(改为手写
  `src/qoder-wasm.ts` 706 行)⇒ 代价在宿主侧;
- 31 import / 34 export 各分四类,逐类给出用途与成员;
- **我们仓库没有 wasm 运行时入口**:`grep -rniE 'wazero|wasmtime|wasmer|
  tetratelabs'` = 0 命中,`go.sum` wasm 命中 = 0,Lua 侧只有注释。

只写事实,未写「已修复」。

## B. 可行性判定(docs/qoder-wasm-integration.md,新文件)

实测推翻了一个此前的假定:**不需要 JS 引擎,纯 Go(wazero)可以驱动它**。
证据来自新增的 tools/qoder-wasm-probe(独立 module,主仓 go.mod 不动):

- 实例化:31 桩,实例化阶段真实调用 0 次;
- level 2 `generate_runtime_auth_fields` → encrypt_user_info/key 各 172 字符;
- level 3 `qodercontext_new` + `prepareInferRequest` → 20 个头,其中
  `Authorization: Bearer COSY.<载荷>.<签名>`(len=209),body 60 字节密文;
- 逐 import 计数:31 个里 **13 个从未被调用**(node/JS 探针居多);
- 量化:≈2.2 ms/请求,线性内存 1.25 MiB,实例可复用(-repeat 50 全 PASS)。

文档含:依赖选型(wazero vs wasmtime-go/wasmer-go,+1.16 MiB 实测)、
接入点设计(走 `__llmsproxy_kernel` 而非新开 Lua 全局,保持「按插件声明授权」)、
31 import 逐个的 Go 实现映射表、6 条 ABI 陷阱(两个 getRandomValues 方向相反、
两套返回布局、requestresult_url 参数顺序、drop_ref 必须 no-op、machineId 必填、
host 不能是 api2-v2)、9 条风险、7 层改动面。

## 为什么本轮不实现

跨 7 层、需新增内核依赖、且三个前置阻塞都不该由 fix 分支暗定:
① wasm 从哪来(不可再分发 / 需运维决策);② 加密端点的**响应也要解密**且是 SSE
逐帧(第二条未实现链路);③ 账号数 0 ⇒ 端到端不可验证。

## 纪律声明

- 未动 main、未开 PR、未部署、未 restart;
- 主仓 `go.mod`/`go.sum` **零改动**(tools/ 是嵌套 module,`go build ./...` 跳过);
- `go test -tags luajit ./...` 全绿(core 420s / gateway 206s);
- **本机 qoder 账号数 0**:能证明「产出了签名头」,不能证明「上游接受」。
  文档内每处 PASS 都标注是离线结论。

## 顺带更正一处过时事实

`internal/core/channels_upstream_gaps_test.go` 注释称 wasm「在参考仓库里也不存在」。
实测:文件存在(sha256 6419471e…),只是不在**我们**仓库。文档已记,本轮不改
代码避免动判据;接入时必须同步演进那条判据(否则新旧逻辑互相打脸)。
2026-10-18 07:36:49 +08:00

679 lines
21 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Command qoder-wasm-probe 评测 `qoder-auth-wasm.wasm` 能否被**纯 Go** wasm
// 运行时(wazero,无 JS 引擎)驱动,并给出它实际需要宿主提供哪些能力。
//
// 这是**取证/评测工具**,不是生产代码:它不注册进网关,也不被内核 import。
// 主仓的 go.mod **不包含** wazero —— 本目录是独立 module。
//
// 为什么需要它:qoder 的加密推理端点头由该 wasm 生成,
// 而 wasm 是 wasm-bindgen 产物(31 个 import 全部来自一个 JS glue 模块)。
// "能不能实例化" 与 "能不能真正跑出鉴权头" 是两件事 —— 上游参考甚至没有
// 提供那份 JS glue(手写了 706 行替代),所以必须实测,不能推断。
//
// 用法:
//
// go run . -wasm /path/to/qoder-auth-wasm.wasm [-json]
//
// 退出码:0 = 三级证据全部通过;1 = 任一环节失败(详见输出)。
package main
import (
"context"
"crypto/rand"
"encoding/binary"
"encoding/json"
"flag"
"fmt"
"math"
"os"
"sort"
"time"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
)
const importModule = "./qoder_auth_wasm_bg.js"
type probe struct {
ctx context.Context
rt wazero.Runtime
mod api.Module
hits map[string]int
order []string
warns []string
strict bool
repeat int
}
// ---- JS-ish 值模型 -------------------------------------------------------
//
// wasm-bindgen 的 ABI 把 JS 值当作整数索引传进导出表。纯 Go 没有这张表,
// 宿主必须自己模拟。qoder 模块实际用到的形状只有三种:
// 1. Uint8Array —— 字节缓冲
// 2. Map —— requestresult_headers 的产物
// 3. crypto 对象 —— 只用来喂 getRandomValues(见 C 类 import)
type (
undefined struct{}
u8arr []byte
jsMap struct{ keys, vals []string }
cryptoObj struct{}
)
// wasm-bindgen 把索引 0..1027 保留给 undefined/null/true/false 等哨兵。
const sentinelN = 1028
func newHeap() []interface{} {
o := make([]interface{}, sentinelN, 4096)
for i := range o {
o[i] = undefined{}
}
o[1024], o[1025], o[1026], o[1027] = undefined{}, nil, true, false
return o
}
func main() {
wasmPath := flag.String("wasm", "",
"qoder-auth-wasm.wasm 路径(必填)")
asJSON := flag.Bool("json", false, "以 JSON 输出结果")
repeat := flag.Int("repeat", 1, "重复调用 prepareInferRequest 的次数(测实例复用)")
strict := flag.Bool("strict", false,
"任何未实现的 import 被调用即失败(默认只告警)")
flag.Parse()
if *wasmPath == "" {
fmt.Fprintln(os.Stderr, "-wasm is required")
os.Exit(2)
}
p := &probe{ctx: context.Background(), hits: map[string]int{}, strict: *strict, repeat: *repeat}
res := p.run(*wasmPath)
if *asJSON {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
enc.Encode(res)
} else {
p.report(res)
}
if !res.OK() {
os.Exit(1)
}
}
type result struct {
WasmPath string `json:"wasm_path"`
WasmSize int `json:"wasm_size"`
Imports int `json:"imports"`
Exports int `json:"exports"`
Instantiated bool `json:"instantiated"`
CallsAtInstall int `json:"calls_at_instantiate"`
AuthFields map[string]any `json:"generate_runtime_auth_fields"`
AuthFieldLens map[string]int `json:"auth_field_lengths"`
AuthErr string `json:"auth_error,omitempty"`
InferURL string `json:"infer_url,omitempty"`
InferBodyLen int `json:"infer_body_len"`
InferHeaders map[string]string `json:"infer_headers,omitempty"`
InferErr string `json:"infer_error,omitempty"`
ImportHits map[string]int `json:"import_hits"`
NeverCalled []string `json:"imports_never_called"`
UnimplementedHit []string `json:"unimplemented_imports_hit,omitempty"`
Warnings []string `json:"warnings,omitempty"`
// MemPages 是跑完三级调用后 wasm 线性内存的页数(1 页 = 64 KiB)。
MemPages uint32 `json:"mem_pages"`
Repeat int `json:"repeat"`
ElapsedMS float64 `json:"elapsed_ms"`
}
func (r result) OK() bool {
return r.Instantiated && r.AuthErr == "" && r.InferErr == ""
}
func (p *probe) heap() []byte { b, _ := p.mod.Memory().Read(0, p.mod.Memory().Size()); return b }
func (p *probe) readStr(ptr, ln uint32) string {
if b, ok := p.mod.Memory().Read(ptr, ln); ok {
return string(b)
}
return ""
}
func (p *probe) warn(format string, a ...any) {
msg := fmt.Sprintf(format, a...)
p.warns = append(p.warns, msg)
fmt.Fprintln(os.Stderr, "warn: "+msg)
}
func (p *probe) run(path string) result {
res := result{WasmPath: path, ImportHits: map[string]int{}}
t0 := time.Now()
b, err := os.ReadFile(path)
if err != nil {
res.InferErr = err.Error()
return res
}
res.WasmSize = len(b)
p.rt = wazero.NewRuntime(p.ctx)
defer p.rt.Close(p.ctx)
cm, err := p.rt.CompileModule(p.ctx, b)
if err != nil {
res.InferErr = "compile: " + err.Error()
return res
}
imports := cm.ImportedFunctions()
res.Imports = len(imports)
res.Exports = len(cm.ExportedFunctions())
// ---- 宿主模块:31 个 import 的纯 Go 实现 ----
host := p.rt.NewHostModuleBuilder(importModule)
for _, f := range imports {
_, name, _ := f.Import()
params, results := f.ParamTypes(), f.ResultTypes()
host.NewFunctionBuilder().
WithGoModuleFunction(api.GoModuleFunc(p.impl(name)), params, results).
Export(name)
}
if _, err := host.Instantiate(p.ctx); err != nil {
res.InferErr = "host module: " + err.Error()
return res
}
p.mod, err = p.rt.InstantiateModule(p.ctx, cm, wazero.NewModuleConfig().WithName("qoder"))
if err != nil {
res.InferErr = "instantiate: " + err.Error()
return res
}
res.Instantiated = true
res.CallsAtInstall = len(p.order) // 实例化阶段真实调用次数
p.level2(&res)
if res.AuthErr == "" {
for i := 0; i < p.repeat; i++ {
p.level3(&res)
if res.InferErr != "" {
res.InferErr = fmt.Sprintf("iteration %d/%d: %s", i+1, p.repeat, res.InferErr)
break
}
}
}
res.ElapsedMS = float64(time.Since(t0).Microseconds()) / 1000
if mem := p.mod.Memory(); mem != nil {
res.MemPages = mem.Size() / 65536
}
res.ImportHits = p.hits
res.NeverCalled = p.neverCalled(imports)
res.Warnings = p.warns
return res
}
// ---- 三级证据 ------------------------------------------------------------
// level2: generate_runtime_auth_fields —— 第一次真实调用。
func (p *probe) level2(res *result) {
payload := `{"uid":"probe-uid","security_oauth_token":"probe-token",` +
`"organization_id":"","organization_tags":[],"data_policy_agreed":false}`
ptr, ln := p.writeString(payload)
raw, err := p.callString("generate_runtime_auth_fields", uint64(ptr), uint64(ln))
if err != nil {
res.AuthErr = err.Error()
return
}
var fields map[string]any
if err := json.Unmarshal([]byte(raw), &fields); err != nil {
res.AuthErr = "non-JSON output: " + err.Error()
return
}
res.AuthFields = map[string]any{}
res.AuthFieldLens = map[string]int{}
for k, v := range fields {
// 只暴露类型与长度:字段本身是敏感材料,不该进日志。
switch s := v.(type) {
case string:
res.AuthFields[k] = fmt.Sprintf("<string len=%d>", len(s))
res.AuthFieldLens[k] = len(s)
default:
res.AuthFields[k] = fmt.Sprintf("<%T>", v)
res.AuthFieldLens[k] = 0
}
}
}
// level3: qodercontext_new + prepareInferRequest —— 产出真实加密请求。
func (p *probe) level3(res *result) {
userInfo := `{"uid":"probe-uid","encrypt_user_info":"probe","key":"probe",` +
`"organization_id":"","organization_tags":[],"data_policy_agreed":false}`
meta := `{"client_type":"10","business_product":"qoder",` +
`"business_type":"default","scene":"chat"}`
mp, ml := p.writeString("probe-machine-uuid")
vp, vl := p.writeString("1.1.49")
up, ul := p.writeString(userInfo)
tp, tl := p.writeString(meta)
sp := p.stackPtr()
_, err := p.export("qodercontext_new").Call(p.ctx, uint64(sp),
uint64(mp), uint64(ml), uint64(vp), uint64(vl),
uint64(up), uint64(ul), uint64(tp), uint64(tl))
v := p.dv()
ctxPtr := int32(v.u32(sp))
isErr := int32(v.u32(sp + 8))
errIdx := int32(v.u32(sp + 4))
p.stackFree()
if err != nil || isErr != 0 {
res.InferErr = fmt.Sprintf("qodercontext_new err=%v isErr=%d obj=%v",
err, isErr, p.getObj(uint32(errIdx)))
return
}
hp, hl := p.writeString("api2.qoder.sh")
bp, bl := p.writeString(`{"messages":[{"role":"user","content":"hi"}]}`)
kp, kl := p.writeString("qfmodel")
skp, skl := p.writeString("system")
sp2 := p.stackPtr()
_, err = p.export("qodercontext_prepareInferRequest").Call(p.ctx, uint64(sp2),
uint64(ctxPtr), uint64(hp), uint64(hl), uint64(bp), uint64(bl),
uint64(kp), uint64(kl), uint64(skp), uint64(skl))
v2 := p.dv()
resPtr := int32(v2.u32(sp2))
rErr := int32(v2.u32(sp2 + 8))
rErrIdx := int32(v2.u32(sp2 + 4))
p.stackFree()
if err != nil || rErr != 0 {
res.InferErr = fmt.Sprintf("prepareInferRequest err=%v isErr=%d obj=%v",
err, rErr, p.getObj(uint32(rErrIdx)))
return
}
hdrIdx, _ := p.export("requestresult_headers").Call(p.ctx, uint64(uint32(resPtr)))
switch hm := p.getObj(uint32(hdrIdx[0])).(type) {
case jsMap:
res.InferHeaders = map[string]string{}
for i := range hm.keys {
k, val := hm.keys[i], hm.vals[i]
// Authorization 是 WASM 产出的签名头:只报形状,不回显载荷。
if len(val) > 24 {
res.InferHeaders[k] = val[:24] + fmt.Sprintf("…<len=%d>", len(val))
} else {
res.InferHeaders[k] = val
}
}
default:
res.InferErr = fmt.Sprintf("requestresult_headers -> %T (expected jsMap)", hm)
}
res.InferURL = p.resultString(resPtr, "requestresult_url")
body := p.resultString(resPtr, "requestresult_body")
res.InferBodyLen = len(body)
res.Repeat = p.repeat
}
func (p *probe) report(r result) {
fmt.Printf("wasm : %s\n", r.WasmPath)
fmt.Printf("size : %d bytes\n", r.WasmSize)
fmt.Printf("imports : %d (all from %q)\n", r.Imports, importModule)
fmt.Printf("exports : %d\n", r.Exports)
fmt.Printf("instantiate: %v calls-at-instantiate=%d\n", r.Instantiated, r.CallsAtInstall)
fmt.Println("\n-- LEVEL 2: generate_runtime_auth_fields --")
if r.AuthErr != "" {
fmt.Println(" FAILED:", r.AuthErr)
} else {
ks := make([]string, 0, len(r.AuthFields))
for k := range r.AuthFields {
ks = append(ks, k)
}
sort.Strings(ks)
for _, k := range ks {
fmt.Printf(" %-20s %s\n", k, r.AuthFields[k])
}
}
fmt.Println("\n-- LEVEL 3: qodercontext_new + prepareInferRequest --")
if r.InferErr != "" {
fmt.Println(" FAILED:", r.InferErr)
} else {
fmt.Printf(" url = %s\n", r.InferURL)
fmt.Printf(" body = %d bytes (encrypted, not logged)\n", r.InferBodyLen)
for _, k := range sortedKeys(r.InferHeaders) {
fmt.Printf(" hdr %-20s %s\n", k, r.InferHeaders[k])
}
}
fmt.Printf("\n-- IMPORT USAGE (%d of %d touched) --\n", len(r.ImportHits), r.Imports)
for _, k := range sortedKeys(r.ImportHits) {
fmt.Printf(" %-50s %d\n", k, r.ImportHits[k])
}
fmt.Printf(" never called: %d\n", len(r.NeverCalled))
for _, n := range r.NeverCalled {
fmt.Printf(" %s\n", n)
}
if len(r.Warnings) > 0 {
fmt.Printf("\n-- WARNINGS (%d) --\n", len(r.Warnings))
for _, w := range r.Warnings {
fmt.Printf(" %s\n", w)
}
}
fmt.Printf("\nRESULT: %s\n", map[bool]string{true: "PASS", false: "FAIL"}[r.OK()])
}
// ---- wasm-bindgen ABI 助手(照 upstream src/qoder-wasm.ts 的语义)---------
func (p *probe) export(n string) api.Function {
f := p.mod.ExportedFunction(n)
if f == nil {
panic("missing export " + n)
}
return f
}
func (p *probe) dv() dataView {
b, _ := p.mod.Memory().Read(0, p.mod.Memory().Size())
return dataView(b)
}
type dataView []byte
func (d dataView) u32(off uint32) uint32 { return binary.LittleEndian.Uint32(d[off:]) }
func (p *probe) stackPtr() uint32 {
out, _ := p.export("__wbindgen_add_to_stack_pointer").Call(p.ctx, api.EncodeI32(-16))
return uint32(api.DecodeI32(out[0]))
}
func (p *probe) stackFree() {
p.export("__wbindgen_add_to_stack_pointer").Call(p.ctx, api.EncodeI32(16))
}
func (p *probe) writeString(s string) (uint32, uint32) {
b := []byte(s)
out, err := p.export("__wbindgen_export2").Call(p.ctx, uint64(len(b)), 1)
if err != nil {
panic(err)
}
ptr := uint32(out[0])
p.mod.Memory().Write(ptr, b)
return ptr, uint32(len(b))
}
// callString 对应「返回值布局 = ptr/len/valIdx/isErr」那一类函数。
func (p *probe) callString(name string, args ...uint64) (string, error) {
sp := p.stackPtr()
defer p.stackFree()
if _, err := p.export(name).Call(p.ctx, append([]uint64{uint64(sp)}, args...)...); err != nil {
return "", fmt.Errorf("%s: %w", name, err)
}
v := p.dv()
ptr, ln := int32(v.u32(sp)), int32(v.u32(sp+4))
if isErr := int32(v.u32(sp + 12)); isErr != 0 {
return "", fmt.Errorf("%s: error idx=%d (%v)", name, int32(v.u32(sp+8)), p.getObj(v.u32(sp+8)))
}
return p.readStr(uint32(ptr), uint32(ln)), nil
}
// resultString 对应 `requestresult_url|body`:参数顺序是 (栈指针, ptr)。
func (p *probe) resultString(result int32, name string) string {
sp := p.stackPtr()
defer p.stackFree()
if _, err := p.export(name).Call(p.ctx, uint64(sp), uint64(uint32(result))); err != nil {
p.warn("%s: %v", name, err)
return ""
}
v := p.dv()
ptr, ln := int32(v.u32(sp)), int32(v.u32(sp+4))
if ptr == 0 {
return ""
}
return p.readStr(uint32(ptr), uint32(ln))
}
// ---- 对象堆 --------------------------------------------------------------
var objects = newHeap()
func pushObj(v any) uint32 {
objects = append(objects, v)
return uint32(len(objects) - 1)
}
func (p *probe) getObj(i uint32) any {
if int(i) < len(objects) {
return objects[i]
}
return undefined{}
}
var dbg = os.Getenv("PROBE_DBG") != ""
func (p *probe) takeObj(i uint32) {
if dbg && i >= sentinelN {
fmt.Printf("[dbg] drop %d (was %T)\n", i, p.getObj(i))
}
if i >= sentinelN && int(i) < len(objects) {
objects[i] = undefined{}
}
}
func (p *probe) asBytes(i uint32) u8arr {
switch v := p.getObj(i).(type) {
case u8arr:
return v
case []byte:
return u8arr(v)
}
p.warn("heap[%d] is %T, expected Uint8Array", i, p.getObj(i))
return nil
}
// ---- 31 个 import 的纯 Go 实现 -------------------------------------------
//
// 分四类(与 docs/qoder-wasm-integration.md 的接口映射表对应):
// A bindgen 对象表 · B 字节缓冲 · C 随机/时间 · D JS 环境探针
func (p *probe) impl(name string) func(context.Context, api.Module, []uint64) {
mark := func(n string) {
p.hits[n]++
if len(p.order) < 4096 {
p.order = append(p.order, n)
}
}
wrap := func(f func(s []uint64)) func(context.Context, api.Module, []uint64) {
return func(_ context.Context, _ api.Module, s []uint64) {
mark(name)
defer func() {
if r := recover(); r != nil {
p.warn("host func %s panicked: %v", name, r)
}
}()
f(s)
}
}
// 未在 auth 路径上验证过的 import:被调用就是新信息,必须记下来。
unimpl := func(what string) func(context.Context, api.Module, []uint64) {
return wrap(func(s []uint64) {
p.warn("import %s reached (%s) -- not exercised by the auth path", name, what)
if p.strict {
panic("unimplemented import reached: " + name)
}
})
}
switch name {
// -- A: bindgen 对象表 ------------------------------------------------
case "__wbindgen_object_drop_ref":
// ⚠️ 必须**不释放**槽位:实测该模块会 drop 一个 Map 之后继续用同一索引
// 调 set(日志见 docs)。wasm-bindgen 新版把 drop 退化为空操作
// (externref 表交给 JS GC),因此这里保持 no-op —— 代价是索引只增不回收。
return wrap(func(s []uint64) {})
case "__wbindgen_object_clone_ref":
return wrap(func(s []uint64) { s[0] = uint64(pushObj(p.getObj(uint32(s[0])))) })
case "__wbindgen_cast_0000000000000001": // wasm slice -> Uint8Array
return wrap(func(s []uint64) {
a, e := uint32(s[0]), uint32(s[1])
dst := make(u8arr, e)
copy(dst, p.heap()[a:a+e])
s[0] = uint64(pushObj(dst))
})
case "__wbindgen_cast_0000000000000002": // wasm str -> JS string
return wrap(func(s []uint64) {
s[0] = uint64(pushObj(p.readStr(uint32(s[0]), uint32(s[1]))))
})
// -- B: 字节缓冲 ------------------------------------------------------
case "__wbg_new_with_length_9cedd08484b73942":
return wrap(func(s []uint64) { s[0] = uint64(pushObj(make(u8arr, uint32(s[0])))) })
case "__wbg_length_0c32cb8543c8e4c8":
return wrap(func(s []uint64) { s[0] = uint64(len(p.asBytes(uint32(s[0])))) })
case "__wbg_subarray_0f98d3fb634508ad":
return wrap(func(s []uint64) {
b := p.asBytes(uint32(s[0]))
lo, hi := uint32(s[1]), uint32(s[2])
if hi > uint32(len(b)) {
hi = uint32(len(b))
}
if lo > hi {
lo = hi
}
s[0] = uint64(pushObj(b[lo:hi]))
})
case "__wbg_set_08463b1df38a7e29":
// 两种形态:Uint8Array.set(src, off) 与 Map.set()(headers 集合)。
return wrap(func(s []uint64) {
switch dst := p.getObj(uint32(s[0])).(type) {
case jsMap:
k, ok := p.getObj(uint32(s[1])).(string)
if !ok {
k = fmt.Sprint(p.getObj(uint32(s[1])))
}
dst.keys = append(dst.keys, k)
dst.vals = append(dst.vals, fmt.Sprint(p.getObj(uint32(s[2]))))
objects[s[0]] = dst
case u8arr:
src := p.asBytes(uint32(s[1]))
off := int(uint32(s[2]))
if off > len(dst) {
return
}
copy(dst[off:], src)
default:
if dbg {
fmt.Printf("[dbg] set arg0=%d type=%T arg2=%d\n", s[0], p.getObj(uint32(s[0])), s[2])
}
p.warn("set on unsupported %T", p.getObj(uint32(s[0])))
}
})
case "__wbg_prototypesetcall_3e05eb9545565046":
return wrap(func(s []uint64) {
a, e := uint32(s[0]), uint32(s[1])
copy(p.heap()[a:a+e], p.asBytes(uint32(s[2])))
})
case "__wbg_new_99cabae501c0a8a0": // new Map()
return wrap(func(s []uint64) {
idx := pushObj(jsMap{})
if dbg {
fmt.Printf("[dbg] newMap -> %d\n", idx)
}
s[0] = uint64(idx)
})
// -- C: 随机源与时间 --------------------------------------------------
//
// ⚠️ 两个 getRandomValues 的签名**方向相反**(upstream 注释里踩过的坑):
// _d49329ff89a07af1 = (wasmPtr, len) -> 直接写 wasm 内存
// _c44a50d8cfdaebeb = (cryptoObjIdx, u8arrIdx) -> 调 JS 对象的方法
// 写反会得到 Rust panic `unreachable`。
case "__wbg_getRandomValues_d49329ff89a07af1":
return wrap(func(s []uint64) {
b := make([]byte, uint32(s[1]))
rand.Read(b)
p.mod.Memory().Write(uint32(s[0]), b)
})
case "__wbg_getRandomValues_c44a50d8cfdaebeb":
return wrap(func(s []uint64) {
if _, ok := p.getObj(uint32(s[0])).(cryptoObj); !ok {
p.warn("getRandomValues(B): arg0 is %T, want cryptoObj", p.getObj(uint32(s[0])))
}
if b := p.asBytes(uint32(s[1])); b != nil {
rand.Read(b)
}
})
case "__wbg_randomFillSync_6c25eac9869eb53c":
return wrap(func(s []uint64) {
if b := p.asBytes(uint32(s[1])); b != nil {
rand.Read(b)
}
})
case "__wbg_now_88621c9c9a4f3ffc":
return wrap(func(s []uint64) {
s[0] = math.Float64bits(float64(time.Now().UnixMilli()))
})
// -- D: JS / Node 环境探针 --------------------------------------------
case "__wbg_crypto_38df2bab126b63dc", "__wbg_msCrypto_bd5a034af96bcba6":
return wrap(func(s []uint64) { s[0] = uint64(pushObj(cryptoObj{})) })
case "__wbg_static_accessor_GLOBAL_THIS_a1248013d790bf5f",
"__wbg_static_accessor_GLOBAL_f2e0f995a21329ff":
return wrap(func(s []uint64) { s[0] = uint64(pushObj(undefined{})) })
case "__wbg_static_accessor_SELF_24f78b6d23f286ea",
"__wbg_static_accessor_WINDOW_59fd959c540fe405":
// 服务端没有 window/self:wasm-bindgen 约定用 0 表示缺席。
return wrap(func(s []uint64) { s[0] = 0 })
case "__wbg___wbindgen_is_object_40c5a80572e8f9d3":
return wrap(func(s []uint64) {
switch p.getObj(uint32(s[0])).(type) {
case u8arr, jsMap, cryptoObj:
s[0] = 1
}
})
case "__wbg___wbindgen_is_string_b29b5c5a8065ba1a":
return wrap(func(s []uint64) {
if _, ok := p.getObj(uint32(s[0])).(string); ok {
s[0] = 1
}
})
case "__wbg___wbindgen_is_undefined_c0cca72b82b86f4d":
return wrap(func(s []uint64) {
if _, ok := p.getObj(uint32(s[0])).(undefined); ok {
s[0] = 1
}
})
case "__wbg___wbindgen_throw_81fc77679af83bc6":
return wrap(func(s []uint64) {
p.warn("wasm throw: %s", p.readStr(uint32(s[0]), uint32(s[1])))
})
case "__wbg_Error_2e59b1b37a9a34c3":
return wrap(func(s []uint64) {
s[0] = uint64(pushObj(fmt.Errorf("%s", p.readStr(uint32(s[0]), uint32(s[1])))))
})
case "__wbg_process_44c7a14e11e9f69e", "__wbg_versions_276b2795b1c6a219",
"__wbg_node_84ea875411254db1", "__wbg_require_b4edbdcf3e2a1ef0",
"__wbg_call_d578befcc3145dee", "__wbg___wbindgen_is_function_49868bde5eb1e745":
return unimpl("node/JS 环境探针")
}
return unimpl("未知")
}
func (p *probe) neverCalled(imports []api.FunctionDefinition) []string {
var out []string
for _, f := range imports {
_, name, _ := f.Import()
if p.hits[name] == 0 {
out = append(out, name)
}
}
sort.Strings(out)
return out
}
func sortedKeys[V any](m map[string]V) []string {
ks := make([]string, 0, len(m))
for k := range m {
ks = append(ks, k)
}
sort.Strings(ks)
return ks
}