v0.1首个正式支持的版本化规范查看版本范围

asset

asset 只访问应用包中由 manifest 声明的只读资源。它解决“guest 如何取得自己的静态文件”和“宿主渲染器如何使用包内字体”,不是通用文件系统或编解码服务。

适用场景

  • 读取应用随包发布的关卡、词典、模板或二进制模型。
  • 将包内字体注册给 canvas 的宿主文本渲染器。
  • 由 guest 自己解码包内图片、音频或压缩数据。

用户选择的文件、拖放内容、网络下载、任意 URL、目录遍历和持久写回都不属于 asset

L1 边界

宿主独占的是已安装应用包的资源索引、只读字节和向宿主文本后端注册字体的效果。guest 无法仅靠计算打开壳层代持的包资源;格式解码、缓存、资源图和场景语义都可在取得字节后由 guest 实现,不进入 L1。

manifest 声明

资源先在包清单中获得稳定的 assetId

{
  "assets": {
    "levels/main": "assets/level-main.bin",
    "fonts/ui": { "path": "assets/ui.woff2", "mediaType": "font/woff2" }
  },
  "capabilities": [
    "host:asset.read",
    "host:asset.loadFont"
  ]
}

assetId 是清单键,不是包内路径。guest 不能把运行时字符串当路径拼接。清单中的包内路径按版本总则使用规范 ASCII 段,不接受百分号编码;因此同一个声明只有一个可请求 URL,不会在壳层 URL 解析时变成另一条路径。

Reference

方法

asset.read

asset.read(assetId, output: ByteSink, offset? = 0) -> {
  written: u32,
  totalSize: u64,
  eof: boolean
}

offset 开始把资源字节写入非空 ByteSink。包资源大小固定且宿主已知,因此始终返回 totalSizewritten 等于 min(output.capacity, asset.maxReadBytes, totalSize - offset)eof 当且仅当 offset + written == totalSizeoffset === totalSize 合法并返回 written: 0, eof: true,更大则为 invalid-offset。调用方以新 offset 继续,不需要先探测大小。

asset.loadFont

asset.loadFont(assetId) -> Handle<font, use, app-context, runtime>

验证、解码并注册包内 WOFF2 字体,返回只能用于宿主文本渲染接口的句柄。v0.1 不接受 TTF、OTF、CSS font-face 或可加载外部资源的字体描述。字体句柄不能被 guest 当字节读取,也不能跨应用传递。同一运行时对同一 assetId 重复调用必须复用同一已验证字体资源,不增加 asset.maxLoadedFonts 计数。

JS 工作流示例

读取一个大小未知的包内二进制资源:

async function readAsset(assetId: string): Promise<Uint8Array> {
  const chunks: Uint8Array[] = [];
  let offset = 0;

  for (;;) {
    const buffer = new Uint8Array(64 * 1024);
    const result = await host.asset.read(assetId, buffer, offset);
    chunks.push(buffer.subarray(0, result.written));
    offset += result.written;
    if (result.eof) return concat(chunks, offset);
  }
}

const levelBytes = await readAsset('levels/main');
const level = decodeLevel(levelBytes); // guest 库负责格式语义

注册字体并交给 canvas

const uiFont = await host.asset.loadFont('fonts/ui');
const [metrics] = await host.canvas.measureText('main', ['开始'], {
  font: uiFont,
  size: 16,
  align: 'start',
  direction: 'ltr',
});

原生 WASM 绑定示例

原生 guest 可以用固定缓冲循环读取,无需一次分配完整资源:

uint8_t chunk[65536];
uint64_t offset = 0;

for (;;) {
  AssetReadResult r = host_asset_read(
    "levels/main",
    byte_sink(chunk, sizeof(chunk)),
    offset
  );
  level_decoder_push(chunk, r.written);
  offset += r.written;
  if (r.eof) break;
}

绑定层只暴露实际写入的字节;未写区域不得被当作宿主返回值。

生命周期

  • 包内资源在应用版本生命周期内不可变。
  • 更新应用版本可以替换同名 assetId 的内容,因此 guest 不得跨版本缓存裸 offset 或内容哈希之外的推断。
  • 字体句柄在当前 runtime 结束前保持有效;宿主不得在仍运行时主动失效它。应用卸载、热重载或 runtime 重启结束整个生命周期。v0.1 不提供提前释放;可加载字体集合由 manifest 有限 assets 和 asset.maxLoadedFonts 共同封闭。
  • 单次读取受 asset.maxReadBytes 限制并在一次调用中完成;v0.1 不为它建立长任务或取消状态机。

错误

code 含义 retryable
not-found assetId 未在本版本 manifest 声明 false
invalid-offset offset 超出资源或不受支持 false
invalid-format 字体格式、魔数或结构校验失败 false
limit-exceeded 读取速率、解码内存或字体数超预算 true

安全与预算

  • 信任档位:green;provenance 为 app-context
  • 宿主必须按 manifest 查表,不能把 assetId 转成未规范化路径后直接打开。
  • 字体先经过大小、格式和结构校验,再交给平台字体后端。
  • v0.1 预算键为 asset.maxReadBytes(单次写入 ByteSink 的字节)、asset.maxFontBytes(单个 WOFF2 文件字节)、asset.maxLoadedFonts(当前 runtime 中不同字体 assetId 的数量)、asset.callsPerMinute(方法调用数)。
  • 资源内容不得包含宿主密钥、平台配置或其他应用的数据。

不属于本 API

图片/音视频解码、压缩、数据库、网络缓存和场景资源管理属于 L2。若以后基准证明某种解码必须宿主加速,应设计只消费明确 ByteSource 的独立能力,不能重新引入能接受任意 file token 的旁路。

一致性测试

宿主至少覆盖:未知 ID、路径穿越字符串、零长度资源、分块边界、空 sink、超大 offset、包升级、损坏字体、字体炸弹、句柄跨应用使用,以及 JS/原生 guest 的字节完全一致性。

Host API 版本与分层概览Host API v0.1版本非正式提议提议canvas API呈现canvas.getInfo()方法canvas.draw()方法canvas.present()方法canvas.measureText()方法canvas.frame event事件canvas.resize event事件asset API呈现asset.read()方法asset.loadFont()方法playback API呈现playback.open()方法playback.write()方法playback.stop()方法playback.ready event事件playback.ended event事件input API输入input.focus()方法input.pointer event事件input.key event事件input.wheel event事件input.focusChange event事件ime API输入ime.openSession()方法ime.updateState()方法ime.updateGeometry()方法ime.closeSession()方法ime.textUpdate event事件ime.composition event事件ime.formatUpdate event事件ime.characterBoundsRequest event事件ime.ended event事件capture API输入capture.requestCamera()方法capture.requestMic()方法capture.frame()方法capture.readAudio()方法capture.stop()方法capture.ended event事件store API数据store.get()方法store.put()方法store.delete()方法store.list()方法store.deletePrefix()方法clipboard API数据clipboard.readText()方法clipboard.writeText()方法env API平台集成env.snapshot()方法env.change event事件开始调用 Host API指南构建交互式绘图应用指南构建自绘文本编辑器指南管理媒体采集与播放指南组织本地数据与同步指南