[WebAssembly 入門] 與 Webpack 聯動


title: [WebAssembly 入門] 與 Webpack 聯動javascript

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

categories: WebAssembly, 筆記java

tags: WebAssembly, JavaScript, Rust, LLVM toolchainnode

auther: Yiniaureact


與 Webpack 聯動


常規的進行rust代碼編寫再手動編譯爲wasm文件是十分緩慢的,目前有幾種解決方案,接下來我將基於webpack來提高WebAssembly的編寫效率。webpack

首先,webpack 4 是必須的,此文寫下時的version是 webpack 4.5.0ios

具體的webpack安裝自行解決git

配置 webpack

建立一個webpack.config.js,輸入以下代碼github

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配置文件web

配置 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,單函數開發的話也能用用

相關文章
相關標籤/搜索