[WebAssembly 入门] 与 Webpack 联动

8,793 阅读2分钟

title: [WebAssembly 入门] 与 Webpack 联动

date: 2018-4-6 19:40:00

categories: WebAssembly, 笔记

tags: WebAssembly, JavaScript, Rust, LLVM toolchain

auther: Yiniau


与 Webpack 联动


常规的进行rust代码编写再手动编译为wasm文件是十分缓慢的,目前有几种解决方案,接下来我将基于webpack来提升WebAssembly的编写效率。

首先,webpack 4 是必须的,此文写下时的version是 webpack 4.5.0

具体的webpack安装自行解决

配置 webpack

创建一个webpack.config.js,输入如下代码

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');

const paths = {
  src: path.resolve(__dirname, 'src'),
  entryFile: path.resolve(__dirname, 'src', 'index.js'),
  dist: path.resolve(__dirname, 'dist'),
  wasm: path.relative(__dirname, 'build'),
}

module.exports = {
  mode: 'development',
  devtool: 'inline-source-map',
  devServer: {
    contentBase: __dirname,
    hot: true,
    port: 10001,
    open: true, // will open on browser after started
  },
  entry: paths.entryFile,
  output: {
    path: paths.dist,
    filename: 'main.js'
  },
  resolve: {
    alias: {
      wasm: paths.wasm,
    }
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: [{
          loader: 'babel-loader',
          options: {
            cacheDirectory: true,
          }
        }],
      },
    ],
  },
  plugins: [
    new CleanWebpackPlugin(['dist']),
    new HtmlWebpackPlugin({
      title: 'WebAssembly Hello World'
    }),
    new webpack.NamedModulesPlugin(),
    new webpack.HotModuleReplacementPlugin(),
  ],
};

配置文件解读就不做了,这是个很基础的配置,如果想要学习进阶配置可以看看create-react-app里eject出来的webpack配置文件

配置 babel

.babelrc

{
  "presets": [
    "env"
  ],
  "plugins": [
    "syntax-dynamic-import",
    "syntax-async-functions"
  ]
}

这里开启了async支持和import()动态导入的支持

要注意的是,静态的import导入.wasm文件并不被webpack内置支持,webpack会在控制台打印错误信息,提示你换成动态导入(Dynamic import)

不添加社区loader支持

webpack 4 内置支持解析 .wasm 文件,并且import不写后缀时搜索的优先级最高

首先让我们看看目录的情况

𝝪 yiniau @ yiniau in /Users/yiniau/code/WebAssembly/hello_world
↪ ll
total 952
-rw-r--r--    1 yiniau  staff    52B  3 25 22:14 Cargo.lock
-rw-r--r--    1 yiniau  staff   143B  4  6 21:51 Cargo.toml
drwxr-xr-x    4 yiniau  staff   128B  4  6 01:36 build
-rw-r--r--    1 yiniau  staff    12B  3 26 21:53 build.sh
-rw-r--r--    1 yiniau  staff   170B  4  4 16:26 index.html
drwxr-xr-x  809 yiniau  staff    25K  4  6 20:34 node_modules
-rw-r--r--    1 yiniau  staff   782B  4  6 20:34 package.json
drwxr-xr-x    3 yiniau  staff    96B  4  4 16:41 rust
drwxr-xr-x    4 yiniau  staff   128B  4  4 17:16 src
drwxr-xr-x    5 yiniau  staff   160B  3 29 15:29 target
-rw-r--r--    1 yiniau  staff   1.2K  4  6 15:51 webpack.config.js
-rw-r--r--    1 yiniau  staff   230K  4  6 01:07 yarn-error.log
-rw-r--r--    1 yiniau  staff   216K  4  6 16:09 yarn.lock

其中

  • rust 中存放 .rs 文件
  • src 中存放 .js 文件
  • build 中存放 .wasm 文件
  • index.js 为 entry 指定的入口文件,我在这里引入polyfill
𝝪 yiniau @ yiniau in /Users/yiniau/code/WebAssembly/hello_world
↪ ll src
total 16
-rw-r--r--  1 yiniau  staff    77B  4  6 20:39 index.js
-rw-r--r--  1 yiniau  staff   1.9K  4  6 21:30 main.js

ok,让我们在main.js中完成主要的逻辑吧

main.js

(async () => {
  import('../build/hello.wasm')
    .then(bytes => bytes.arrayBuffer())
    .then(res => WebAssembly.instantiate(bytes, imports))
    .then(results => {
      console.log(results);
      const exports = results.instance.exports;
      console.log(exports);
      mem = exports.memory;
    });
})()

oh heck!! 为什么会报错!!

没事,错误信息很明确

WebAssembly.Instance is disallowed on the main thread, if the buffer size is larger than 4KB. Use WebAssembly.instantiate.

如果 buffer的大小超过了 4KBWebAssembly.Instance 在主线程中不被允许使用。需要使用WebAssembly.instantiate代替,但是问题来了。

import() 并不能传递 importsObject。让我们去 webpack 的github上找找看issue:

github issue

linclark(a cartoon to WebAssembly 的作者) 提出使用 instantiateStreaming 代替 compileStreaming,以避免在ios上因快速内存的限制造成的影响。

sokra 对此表示有点反对(应该是非常反对!)

不支持的原因

预备信息

webpack试图像ESM一样对待WASM。 将适用于ESM的所有规则/假设也应用于WASM。假设将来WASM JS API可能会被W​​ASM集成到ESM模块图中。

这意味着WASM文件中的imports部分(importsObject)与ESM中的import语句一样被解析,exports部分(instance.exports)被视为像ESM中的export部分。

WASM模块也有一个start部分,在WASM实例化时执行。

在WASM中,JS API的导入通过importsObject传递给实例化的WASM模块。

ESM规范

ESM规范指定了多个阶段。一个阶段是ModuleEvaluation。在这个阶段,所有的模块都按照明确的顺序进行评估。这个阶段是同步的。所有模块都以相同的“tick”进行评估。

当WASM在模块图中时,这意味着:

  • start部分在相同的“tick”中执行
  • WASM的所有依赖关系都在相同的“tick”中执行
  • 导入WASM的ESM在相同的“tick”中执行

对于使用Promise的instantiate,这种行为是不可能的。一个Promise总是将它的履行延迟到另一个“tick”中。

只有在使用实例化同步版本(WebAssembly.Instance)时才可能。

注意:从技术上讲,可能会有一个没有start部分并且没有依赖关系的WASM。在这种情况下,这不适用。但我们不能认为情况总是如此。

webpack想要并行下载/编译wasm文件与下载JS代码。使用instantiateStreaming不会允许这样做(当WASM具有依赖关系时),因为实例化需要传递一个importsObject。创建importsObject需要评估WASM的所有依赖项/导入,因此需要在开始下载WASM之前下载这些依赖项。

当使用compileStreaming + new WebAssembly.Instance并行下载和编译是可能的,因为compileStreaming不需要一个importsObject。可以在WASM和JS下载完成时创建importsObject

WebAssembly规范

我也想引用WebAssembly规范。它指出编译发生在后台线程中compile和实例化发生在主线程上。

它没有明确说明,但在我看来JSC的行为是不规范的。

其他说明

WASM也缺乏使用导入标识符的实时绑定的能力。相反,importsObject将被复制。这可能会伴随奇怪的循环依赖和WASM上的问题。

importsObject中支持getter并且能够在执行start部分之前获得exports会更好。

尝试使用loader直接解析.rs

wasm-loader 对于直接使用 rustup wasm32-unknown-unknown 编译的.wasm文件支持有问题,看了下wasm-loader使用了基于emcc工具链产出的wasm文件,我试过直接使用

rules: [
  {
    test: /\.(js|jsx)$/,
    exclude: /node_modules/,
    use: [{
      loader: 'babel-loader',
      options: {
        cacheDirectory: true,
      }
    }],
  },
  {
    test: /\.wasm$/,
    include: path.resolve(__dirname, 'wasm'),
    use: 'wasm-loader',
  },
],

但是会报错:

这应该是工具链产出的编码问题

于是我再次尝试了使用rust-native-wasm-loader

webpack.config.js

rules: [
  {
    test: /\.(js|jsx)$/,
    exclude: /node_modules/,
    use: [{
      loader: 'babel-loader',
      options: {
        cacheDirectory: true,
      }
    }],
  },
  {
    test: /\.rs$/,
    include: paths.rust,
    use: [{
      loader: 'wasm-loader'
    }, {
      loader: 'rust-native-wasm-loader',
      options: {
        release: true,
      },
    },]
  },
],

rust/add.rs

#[no_mangle]
pub fn add(a: i32, b: i32) -> i32 {
    eprintln!("add({:?}, {:?}) was called", a, b);
    a + b
}

main.js

import loadAdd from 'rust/add.rs';

loadAdd().then(result => {
  const add = result.instance.exports['add'];
  console.log('return value was', add(2, 3));
});

BUT!

臣妾做不到啊!!

我已经完全按照rust-native-wasm-loader的例子改了,但似乎现在的插件都是在asm.js时代遗留的,都是在解析成.wasm那一步失败,是因为WebAssembly不适合以同步方式调用吗。。就目前来看,如果在rust中调用std::mem来操作Memory对象,文件大小会非常大——使用wasm-gc后依旧有200多的KB

#![feature(custom_attribute)]
#![feature(wasm_import_memory)]
#![wasm_import_memory]

use std::mem;
use std::ffi::{CString, CStr};
use std::os::raw::{c_char, c_void};

/// alloc memory
#[no_mangle]
// In order to work with the memory we expose (de)allocation methods
pub extern fn alloc(size: usize) -> *mut c_void {
    let mut buf = Vec::with_capacity(size);
    let ptr = buf.as_mut_ptr();
    mem::forget(buf);
    ptr as *mut c_void
}

或许webpack的做法并不适合全部的web assembly的应用模式,以ESM的方式处理.wasm似乎很美好,但是实际使用可能会成问题,目前主要还是js处理逻辑,为了兼容低版本浏览器使用异步处理(或许是)必须的?

2018-4-17 12:00 更新

Parcel!

webassembly的webpack支持PR有更新了!一句不起眼的tip I don't know if this helps but it seems parceljs has got support for rust functions. BY pyros2097 https://medium.com/@devongovett/parcel-v1-5-0-released-source-maps-webassembly-rust-and-more-3a6385e43b95

ok

我滚去用Parcel了...

虽然不能传imports,单函数开发的话也能用用