WebAssembly 预取外部资源,然后使用 fopen 进行操作。

3
我有一个使用EMSDK将C++项目编译为WebAssembly的项目。我有一个资产,我的项目加载它,一个bundle.zip,我目前在编译时使用SDK的'--embed-file'标志,这样可以正常工作。
然而,我想知道是否有一种在HTML或JavaScript中预加载此资产的方法。所以,不必每次在'bundle.zip'中更改内容时都要重新构建我的项目,我只需上传它并保持相同的'.wasm'文件。
这个选项是否可用?在网上搜索时,我只找到与C#的Blazor相关的问题和答案,这与我想要的不相关。
一个关键的细节是,我的应用程序需要从文件系统中读取此文件,就像它在本机平台上一样,而不是在Web上(它执行fopen操作)。

--preload-file不就是做这个的吗? - undefined
2个回答

1
你可以考虑在运行时使用JavaScript来获取'bundle.zip'文件,然后使用Emscripten文件系统API使其对你的WebAssembly模块可访问。 这样,你可以在不重新编译WebAssembly模块的情况下更改'bundle.zip'文件。
+-------------+     +-----------------+     +-------------+
| C++ Project | --> | Compile to WASM | --> | Web Browser |
+-------------+     +-----------------+     +-------------+
      |                   |                        |
      |                   |                        |
      |                   |                        +-- Fetch bundle.zip
      |                   |                        |   at runtime
      |                   |                        |
      |                   |                        +-- Write to Virtual FS
      |                   |                        |   (Emscripten FS API)
      |                   |                        |
      |                   |                        +-- Access via fopen()

首先,从你的 Emscripten 构建命令中移除 --embed-file 标志。这将停止将 'bundle.zip' 嵌入到你的 WebAssembly 模块中。
检查一下,如评论所述,Emscripten » Porting » Files and File Systems » Packaging Files 中提到的 --preload-file 选项是否可行。
在使用 Emscripten 编译你的项目时,使用 --preload-file 标志,后面跟上 'bundle.zip' 的路径。然后,Emscripten 会在你的 '.wasm' 和 '.js' 文件旁生成一个 '.data' 文件。
emcc your_project.cpp -o your_project.html --preload-file path/to/bundle.zip

生成的JavaScript加载器代码将在加载WebAssembly模块时自动获取“.data”文件。这意味着“bundle.zip”将与您的“.wasm”文件分开下载,但对于您的应用程序来说,这是透明的。
“bundle.zip”的内容将在运行时自动解压到Emscripten虚拟文件系统中。然后,您的应用程序应使用fopen来访问这些文件,就像它们在常规文件系统上一样。
要更新“bundle.zip”,您只需在服务器上替换“.data”文件。除非C++代码本身发生更改,否则无需重新编译WebAssembly模块。
或者在网页加载时使用JavaScript获取'bundle.zip'。您可以使用fetch API来实现这一点。
在调用WebAssembly模块中需要'bundle.zip'的任何函数之前,请确保挂载一个虚拟文件系统并将获取的文件写入其中:这将使'bundle.zip'可访问,就像它是普通文件系统的一部分一样。
您的C++代码现在应该能够使用fopen打开'bundle.zip',就像在本地文件系统上一样。
// Fetch the bundle.zip
fetch('path/to/bundle.zip')
  .then(response => response.arrayBuffer())
  .then(data => {
    // Convert the fetched data into a Uint8Array
    var uint8View = new Uint8Array(data);

    // Initialize the Emscripten file system
    FS.writeFile('/bundle.zip', uint8View);

    // Now you can call your WebAssembly functions that use fopen
    // For example: Module.ccall('yourFunction', 'returnType', ['arg1Type'], [arg1Value]);
  })
  .catch(err => console.error('Error fetching bundle.zip:', err));

FS.writeFile(Emscripten文件系统API的一部分)用于将获取的文件写入Emscripten虚拟文件系统。


OP的答案提出了一个很好的解决方案的实现。

<head>部分使用<link rel="preload" href="/bundle.zip" as="fetch" ...>提示浏览器'bundle.zip'是一个即将需要的资源,这可以提高加载性能,因为浏览器可以在页面加载过程中尽早开始获取它。
在HTML主体中设置了一个<canvas>元素,很可能用于渲染来自WebAssembly模块的内容。

CSS确保了一致的盒模型,并重置了默认的边距,同时为canvas和容器元素设置了样式。

Module对象正确配置了canvas元素,并将noInitialRun设置为true,表示WebAssembly模块不应立即在加载时运行。使用onRuntimeInitialized回调在WebAssembly环境初始化后获取'bundle.zip'。

fetch API 用于将 'bundle.zip' 加载到 ArrayBuffer 中。然后将该缓冲区转换为 Uint8Array,并使用 FS.writeFile 写入到 Emscripten 虚拟文件系统的路径 '/bundle.zip'。一旦 'bundle.zip' 被写入文件系统,就会调用 Module.callMain() 来启动主应用程序。

最后,包含了脚本 carimbo.js,这很可能是由 Emscripten 生成的 JavaScript,负责加载和运行 WebAssembly 模块。


1
在@VonC的回复之后,这是我的最终HTML。
<!doctype html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <link rel="preload" href="/bundle.zip" as="fetch" type="application/octet-stream" crossorigin="anonymous" />
  <style>
    *,
    *::before,
    *::after {
      box-sizing: border-box;
    }

    * {
      margin: 0;
    }

    body {
      line-height: 1.5;
      -webkit-font-smoothing: antialiased;
    }

    canvas {
      display: block;
      max-width: 100%;
    }

    .container {
      display: flex;
      align-items: center;
      justify-content: center;
    }
  </style>
</head>

<body>
  <div class="container">
    <canvas id="canvas" oncontextmenu="event.preventDefault()"></canvas>
    <script>
      const canvas = document.getElementById("canvas");

      var Module = {
        canvas,
        noInitialRun: true,
        onRuntimeInitialized: () => {
          fetch("/bundle.zip")
            .then((response) => response.arrayBuffer())
            .then((data) => {
              const uint8View = new Uint8Array(data);
              FS.writeFile("/bundle.zip", uint8View);
              Module.callMain();
            });
        },
      };
    </script>
    <script src="carimbo.js"></script>
  </div>
</body>

</html>

1
干得好,点赞。我在我的回答中加入了一些对你的实现的评论。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接