WASM与OPFS组合技系列二(魔改读操作)

1、简介

上一期主要介绍了怎么将文件保存到OPFS中,这期主要介绍怎么修改emcc生成的wasm胶水文件,使wasm内可以直接读取opfs中的文件,从而避免直接将大文件直接读取内存,导致内存爆炸问题。

2、emcc介绍

emcc是Emscripten SDK安装后生成的命令,用于将C或C++编译为wasm文件,在编译时还可以同步生成对应的JS胶水文件和对应的测试的html文件,如果不生成JS文件,就需要自己编写JS代码去实现与wasm的交互。默认情况下直接使用生成的胶水文件。

对于Emscripten SDK的安装可以参考官方文档说明:安装说明链接

3、简单的文件读取的wasm示例

file.c 代码

C 复制代码
#include <stdio.h>
#include <stdlib.h>

int read_file(const char *file){
    FILE *fp,*fpbuff;
    if (!(fpbuff=fopen(file,"r"))) {
        printf("read file fail:%s\n",file);
        return 0;
    }
    fseek(fpbuff, 0, SEEK_END);
    long length = ftell(fpbuff);
    fseek(fpbuff, 0, SEEK_SET);
    char *conf_data = (char*)malloc(length);
    fread(conf_data, 1, length, fpbuff);
    fp = fmemopen(conf_data, length , "r");
    if (!fp) {
        free(conf_data);
        return 0;
    }
    char buff[1024];
    while (fgets(buff,1024,fp)) {
        printf("line str : %s\n",buff);
    }
    fclose(fpbuff);
    fclose(fp);
    free(conf_data);
    printf("read file end : %s\n",file);
    return 1;
}

编译C文件

shell 复制代码
emcc file.c  -o file.js -O0 --js-library "d:/wasm/emsdk-main/upstream/emscripten/src/lib/libworkerfs.js" -s EXPORTED_FUNCTIONS="['_read_file','_malloc','_free']" -s FORCE_FILESYSTEM=1 -s TOTAL_MEMORY=1024MB -lworkerfs.js -s ENVIRONMENT=worker

其中--js-library命令后面指向的JS文件地址需要更改为自己本地的地址。

4、魔改胶水文件

当执行过上面的编译操作后,会生成file.js和file.wasm两个文件,现在我们需要对file.js进行魔改,为了降低后续影响,不推荐直接修改file.js文件,应该通过覆盖file.js中的FS的属性方法来实现改造。

a) 编写扩展方法 extendFSMethod

JS 复制代码
const extendFSMethod = (FS) => {
  const WORKERFS = FS.filesystems.WORKERFS;
  if (WORKERFS) {
    WORKERFS.mount = function (mount) {
      WORKERFS.reader ??= new FileReaderSync();
      const root = WORKERFS.createNode(null, "/", WORKERFS.DIR_MODE, 0);
      const createdParents = {};
      function ensureParent(path) {
        const parts = path.split("/");
        let parent = root;
        for (let i = 0; i < parts.length - 1; i++) {
          const curr = parts.slice(0, i + 1).join("/");
          createdParents[curr] ||= WORKERFS.createNode(
            parent,
            parts[i],
            WORKERFS.DIR_MODE,
            0
          );
          parent = createdParents[curr];
        }
        return parent;
      }
      function base(path) {
        const parts = path.split("/");
        return parts[parts.length - 1];
      }
      Array.prototype.forEach.call(mount.opts["files"] || [], function (file) {
        WORKERFS.createNode(
          ensureParent(file.name),
          base(file.name),
          WORKERFS.FILE_MODE,
          0,
          file,
          file.lastModifiedDate
        );
      });
      (mount.opts["blobs"] || []).forEach((obj) => {
        WORKERFS.createNode(
          ensureParent(obj["name"]),
          base(obj["name"]),
          WORKERFS.FILE_MODE,
          0,
          obj["data"]
        );
      });
      (mount.opts["accessHandles"] || []).forEach((obj) => {
        WORKERFS.createNode(
          ensureParent(obj["name"]),
          base(obj["name"]),
          WORKERFS.FILE_MODE,
          0,
          obj["handle"]
        );
      });
      (mount.opts["packages"] || []).forEach((pack) => {
        pack["metadata"].files.forEach((file) => {
          const name = file.filename.substr(1);
          WORKERFS.createNode(
            ensureParent(name),
            base(name),
            WORKERFS.FILE_MODE,
            0,
            pack["blob"].slice(file.start, file.end)
          );
        });
      });
      return root;
    };
    WORKERFS.createNode = function (parent, name, mode, dev, contents, mtime) {
      const node = FS.createNode(parent, name, mode);
      node.mode = mode;
      node.node_ops = WORKERFS.node_ops;
      node.stream_ops = WORKERFS.stream_ops;
      node.timestamp = (mtime || new Date()).getTime();
      if (mode === WORKERFS.FILE_MODE) {
        if (contents instanceof FileSystemSyncAccessHandle) {
          node.size = contents.getSize();
        } else {
          node.size = contents?.size || 0;
        }
        node.contents = contents;
      } else {
        node.size = 4096;
        node.contents = {};
      }
      if (parent) {
        parent.contents[name] = node;
      }
      return node;
    };
    WORKERFS.stream_ops.read = function (
      stream,
      buffer,
      offset,
      length,
      position
    ) {
      console.log("文件名称:", stream.node.name);
      if (position >= stream.node.size) return 0;
      if (stream.node.contents instanceof FileSystemSyncAccessHandle) {
        const size = stream.node.size;
        let byteSize = length;
        if (position + length > size) {
          byteSize = size - position;
        }
        console.log("文件总大小:", size);
        console.log("本次读取大小:", byteSize);
        const unit8Array = new Uint8Array(byteSize);
        stream.node.contents.read(unit8Array, {
          at: position,
        });
        buffer.set(unit8Array, offset);
        return unit8Array.length;
      } else {
        const chunk = stream.node.contents.slice(position, position + length);
        const ab = WORKERFS.reader.readAsArrayBuffer(chunk);
        buffer.set(new Uint8Array(ab), offset);
        return chunk.size;
      }
    };
  }
};

b) 覆盖FS对象属性

在worker中加载完file.js后,就会在全局创建ModuleFS对象,这时候就可以覆盖FS对象属性了。

JS 复制代码
importScripts("./file.js");
extendFSMethod(FS);

5、在worker中编写读取文件的代码

JS 复制代码
const readFile = async (filename) => {
  Module.print("\n ----------------------------------------------- \n");
  Module.print("准备读取文件:", filename);
  const root = await navigator.storage.getDirectory();
  try {
    const dir = "/tmp/" + new Date().getTime();
    FS.mkdir(dir);
    const file = await root.getFileHandle(filename);
    const handle = await file.createSyncAccessHandle();
    const size = (handle.getSize() / 1024 / 1024).toFixed(2);
    Module.print("文件大小:", size, "M");
    // 将文件挂载到虚拟文件系统中
    FS.mount(
      FS.filesystems.WORKERFS,
      {
        accessHandles: [
          {
            name: filename,
            handle,
          },
        ],
      },
      dir
    );
    const path = dir + "/" + filename;
    const textEncoder = new TextEncoder();
    const configByte = textEncoder.encode(path);
    const configByteLength = configByte.length;
    const configPointer = Module._malloc(configByteLength);
    Module.HEAPU8.set(configByte, configPointer);
    Module.HEAPU8[configPointer + configByteLength] = 0;
    let startTime = new Date().getTime();
    Module._read_file(configPointer);
    let endTime = new Date().getTime();
    Module.print(
      "读取文件总共耗时:",
      ((endTime - startTime) / 1000).toFixed(3),
      "秒"
    );
    Module._free(configPointer);
    // 一定要在使用后释放文件句柄
    handle.close();
  } catch (e) {
    if (e.name === "NotFoundError") {
      Module.print("OPFS缓存中不存在该文件,", filename);
    }
  }
};

这里有几个关键点

(1)在虚拟文件系统中挂载文件时需要在/tmp目录下创建一个新的目录挂载

(2)在使用完文件后必须释放文件句柄,不释放再次获取该文件句柄将会失败

6、整体流程结构

7、执行效果

8、仓库代码

代码地址

相关推荐
清秋30 分钟前
全网最全 ECMAScript 攻略( 更新至 ES2026)
前端·javascript·编程语言
Cao_Ron1 小时前
用 SVG image symbol 画 ECharts graph 圆形节点
前端
hxy06011 小时前
React-Call让你的React组件像异步函数一样调用
前端·react.js
Hilaku1 小时前
为什么 AI 写前端时,总是优先选择 React,而不是 Vue?
前端·javascript·程序员
京东云开发者1 小时前
GPT-5.6、ChatGPT Work 与 Codex 更新食用指南
前端
Cao_Ron1 小时前
用 ECharts graph 画出卡片式矩形节点
前端
skiyee1 小时前
换个底座的 Wot Starter 居然让 AI 更懂项目!
前端·ai编程
anyup1 小时前
这一套绝了!AI 大模型写故事,星云 SDK 驱动 3D 数字人实时演绎
前端·人工智能·aigc
耀耀切克闹灬2 小时前
短链跳转
前端
孟陬2 小时前
高质量全方位的 AI 工具 sse-stuntman — Mock SSE 接口
前端·node.js·openai