[WebAssembly 入門] 與 Webpack 聯動

Yiniau發表於2018-04-10

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 入門] 與 Webpack 聯動

沒事,錯誤資訊很明確

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的行為是不規範的。

[WebAssembly 入門] 與 Webpack 聯動

[WebAssembly 入門] 與 Webpack 聯動

其他說明

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',
  },
],
複製程式碼

但是會報錯:

[WebAssembly 入門] 與 Webpack 聯動

這應該是工具鏈產出的編碼問題

於是我再次嘗試了使用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!

[WebAssembly 入門] 與 Webpack 聯動

臣妾做不到啊!!

我已經完全按照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,單函式開發的話也能用用

相關文章