蒼時弦也
蒼時弦也
資深軟體工程師
發表於

忙裡偷閒玩了一下 Emscripten 將 mruby 拉到 Web 上面運行。

最初是看到 WebRuby 這個專案的應用 Webirb 才決定要挑戰將 mruby 丟到 Web 上面跑。

其實這個過程中 WebRuby 給我很多參考方向,才讓我得以順利完成 mruby on Web 的挑戰。

在兩年前(2013)的 JSDC 上,我首次得知了 ASM.jsEmscripten 這兩個專案,當時只知道是一個可以把 C/C++ 的運行效能帶到 Web 上的專案,卻不得其門而入。

不過最近再次回想起來,重新閱讀了一次 Emscripten 的文件後終於瞭解到入門的基礎用法。

也許是兩年前的英文程度不好吧,或者當時因為沒有接觸太多 C/C++ 因此無法理解其深意。

總而言之,在學會使用 Emscripten 的同時,我最想拉到網頁上的就是 mruby 了(作為第一次跟 C/C++ 整合的語言,讓我很興奮,另外 Ruby 也是我目前最喜歡的語言 XD)

安裝 Emscripten

基本上依照官網的安裝教學應該就可以很順利的完成安裝。

我自己是放在 ~/Workspace/Emscripten 下面,要使用的時候就先跑一次 source ./emSDK_env.sh 來讓 emcc 等指令可以使用。

SDK 的設計並不是很好,所以一定得在 SDK 目錄下跑 source 指令

目錄結構

單純是我習慣的配置,如果大家有自己的習慣可以修改。

  • mruby.js
    • mruby/ - 原始碼
    • build/ - 輸出檔案
      • src/ - mruby 的 .o 檔案
        • gems/ Gem 的 .o 檔案
      • dest/ 輸出的 .html .js 檔案
    • mrbjs.c - 應用 mruby 的 c 檔案( main )
    • Makefile

取得 mruby

mruby 是輕量的 ruby 套件,編譯完成的 mruby.js 加上一些預設的 gems (像是 puts 等支援)大約才 1.3 MB 左右,算是還在接受的範圍內。

首先,先把 mruby 的原始碼下載下來。

git clone [email protected]:mruby/mruby.git

獲得原始碼後,我們需要配置一下 build_config.rb 這個檔案,加入名為 emscripten 的編譯器 toolchain 來產生可供網頁使用的版本。

 1# 略
 2
 3# 以下配置是參考 WebRuby 的配置
 4MRuby::Toolchain.new(:emscripten) do |conf| # 定義 Toolchain: emscripten
 5  toolchain :clang # 從 clang 的 toolchain 繼承配置
 6
 7  conf.cc do |cc| # 修改編譯設定
 8    cc.command = "emcc" # 將原本的 cc 改為 emscripten 的 emcc
 9    cc.flags.push(%w(-Wall -Werror-implicit-function-declaration -Wno-warn-absolute-paths -O0)) # 增加編譯的 flag
10  end
11
12	# 其他編譯選項的配置
13  conf.cxx.command = "emcc"
14  conf.linker.command = "emcc"
15  conf.archiver.command = "emar"
16end
17
18MRuby::CrossBuild.new("emscripten") do |conf| # 增加 mruby 編譯任務(跨平台類型的編譯)
19  toolchain :emscripten # 使用 Emscripten 編譯
20
21  conf.build_dir = File.expand_path('build/emscripten') # 設定編譯完成後檔案輸出位置
22  conf.gembox 'default' # 選擇要一同編譯的 gem (Default 會包括 Ruby 預設的基本 Class 和 Method 在裡面)
23end

完成之後運行 rake 指令就會自動編譯完成,此時理論上要在 build/emscripten 目錄看到編譯完成的 static library 等檔案。

這邊要注意的是 cc.flags.push 項目加入了 -O0 這個 flag 之後會說明為什麼會使用 -O0

mrbjs.c

這邊寫一個簡單的 C 去呼叫 mruby 執行一段 Ruby Code 後續的應用就交給大家想像了~

 1#include <stdio.h>
 2
 3#ifdef __EMSCRIPTEN__
 4#include <emscripten.h>
 5#endif
 6
 7#include "mruby.h"
 8#include "mruby/compile.h"
 9#include "mruby/string.h"
10
11void run(mrb_state* mrb, const char* code) {
12
13  mrb_value result = mrb_load_string(mrb, code);
14
15  const char* return_str;
16
17  if(mrb->exc) {
18    return_str = mrb_string_value_ptr(mrb, mrb_obj_as_string(mrb, mrb_obj_value(mrb->exc)));
19    printf("%s\n", return_str);
20  }
21
22}
23
24int main(int argc, char** argv) {
25  mrb_state* mrb = mrb_open();
26  webmrb_run(mrb, "puts \"Hello World\"");
27  mrb_close(mrb);
28  return 0;
29}

Makefile

因為指令挺繁瑣的,所以就臨時抱佛腳看了一下 Make 命令教程這篇讓我印象深刻的教學(清楚簡單的說明用法,個人很喜歡)學了基本的 Makefile 撰寫方法。

 1MRB_SRC=mruby/build/emscripten
 2MRB_O=$(MRB_SRC)/src/*.o
 3MRBLIB_O=$(MRB_SRC)/mrblib/*.o
 4GEMS=$(MRB_SRC)/mrbgems # Gems 比較特別,另外就是我找不到好方法可以 Nested 的複製 .o 檔案(有人知道請告訴我)
 5
 6.PONHY: build mrbsrc clean gems init
 7
 8init:
 9  mkdir build
10  mkdir build/src
11  mkdir build/dest
12
13build: init mrbsrc gems mrbjs.o
14	emcc ./build/src/*.o ./build/src/gems/*.o ./build/src/gems/**/*.o ./build/src/gems/**/src/*.o -o build/dest/mrbjs.html -O2 --memory-init-file 0
15
16mrbjs.o: mrbjs.c
17  emcc mrbjs.c -I mruby/include -o ./build/src/mrbjs.o -O2  -Wall -Werror-implicit-function-declaration  -Wno-warn-absolute-paths
18
19mrbsrc:
20  cp $(MRB_O) build/src
21  cp $(MRBLIB_O) build/src
22
23gems:
24  cp -r $(GEMS) build/src/gems
25
26clean:
27  rm -rf build/*

之後運行 make build 就可以在 build/dest 目錄看到 mrbjs.html 這個檔案,開啟後如果正常運作就會看到出現 Hello World 的字樣在畫面上。

我想大家可能會發現 -O2 這個 flag,跟前面的 -O0 又有什麼關係呢?後面的 FAQ 會解釋這個問題。

FAQ

Q: -O0-O2 到底是什麼?

這是 Emscripten 的 Optimize 設定項目,詳細可以參考 Emscripten - Optimizing Code 這個頁面。

前面在編譯 mruby 的時候會使用 -O0 (不優化)是因為 nested structs展開問題。不過我去追 Google 的 PNaCI 關於這個的討論串,似乎已經在去年年底解決了(Emscripten 專案何時支援還不確定)

簡單說用了 -O2 (release suggest) 的優化,在編譯的時候(產生 html/js)就會發生錯誤。

個人認為這是 mruby on Web 的效能貧頸,經過我自己簡單的 Benchmark ( (1...10000).each { |i| i } ) 在網頁上跑大概需要花 29ms ~ 33ms 但是一般的 Ruby 可以在 0.5ms 內完成,速度上差了快一百倍。 不過我也很樂見在 -O2 支援的同時這個問題理論上會被解決掉,因為能使用 asm.js 的加速,還有 Emscripten 的優化,照理說不應該慢到哪裡去(目前的問題應該在 libmruby 太慢)

Q: --memory-file-init 是什麼?

個人推測是預先將一些靜態的結果先產生好,在執行時直接運行結果而非動態執行的設定,在這邊會設定為 0(關閉)是因為在 WebRuby 中有註解提及這會讓網站卡住,如果有興趣的話可以取消這個設定運行看看是否能正常運行。

小結

我距離土炮 RPG Maker 又更進一步了,雖然試了很多方法果然還是在 Web 上覺得最舒適啊 XD (Emscripten 已經做好 SDL 的 Port 可以說是一大福音啊~~)