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…),只是不在**我们**仓库。文档已记,本轮不改
代码避免动判据;接入时必须同步演进那条判据(否则新旧逻辑互相打脸)。
This commit is contained in:
dev
2026-10-18 07:36:49 +08:00
parent 4174310421
commit d3bb47bd54
5 changed files with 1172 additions and 1 deletions

View File

@ -0,0 +1,678 @@
// 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
}