忙裡偷閒玩了一下 Emscripten 將 mruby 拉到 Web 上面運行。
最初是看到 WebRuby 這個專案的應用 Webirb 才決定要挑戰將 mruby 丟到 Web 上面跑。
其實這個過程中 WebRuby 給我很多參考方向,才讓我得以順利完成 mruby on Web 的挑戰。
在兩年前(2013)的 JSDC 上,我首次得知了 ASM.js 跟 Emscripten 這兩個專案,當時只知道是一個可以把 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
檔案
- gems/ Gem 的
- dest/ 輸出的
.html
.js
檔案
- src/ - mruby 的
- 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 可以說是一大福音啊~~)