翻譯:雲荒杯傾
本文是Emscripten-WebAssembly專欄系列文章之一,更多文章請查看專欄。
也能夠去做者的博客閱讀文章。
歡迎加入Wasm和emscripten技術交流羣,羣聊號碼:939206522。html
Emscripten提供了多種方法來鏈接和交互JavaScript和編譯的C或c++,本文逐一介紹。node
JavaScript中調用編譯的C函數的最簡單的方法是使用ccall()和cwrap()。c++
ccall()用具體的參數和返回值調用一個編譯的C函數,而cwrap()是一個編譯的C函數的包裹,調用它會返回一個JavaScript能夠調用的函數。若是你打算屢次調用一個函數的話,cwrap()用處更大。git
舉個例子, 下面代碼是tests/hello_function.cpp文件。int_sqrt()函數外面套了個extern "C"是爲了防止C++對它的名字改編。github
#include <math.h> extern "C" { int int_sqrt(int x) { return sqrt(x); } }
使用的編譯命令爲:segmentfault
./emcc tests/hello_function.cpp -o function.html -s EXPORTED_FUNCTIONS="['_int_sqrt']"
編譯完,你可使用JavaScript調用cwrap()拿到int_sqrt函數。繼而能夠進行其餘操做。api
int_sqrt = Module.cwrap('int_sqrt', 'number', ['number']) int_sqrt(12) int_sqrt(28)
第一個參數是函數名,第二個參數是函數返回類型,第三個是參數類型。
返回類型和參數類型中能夠用類型有三個,分別是number,string和array。number(是js中的number,對應着C中的整型,浮點型,通常指針),string(是JavaScript中的string,對應着C中的char,C中char表示一個字符串),array(是js中的數組或類型數組,對應C中的數組;若是是類型數組,必須爲Uint8Array或者Int8Array)。數組
編譯完,你能夠運行function.html,在瀏覽器先看一看,實際上啥也沒有,由於tests/hello_function.cpp文件沒有主函數(main())。打開一個js開發環境,敲下下面上面的幾行,就能看到運行結果,12開方爲3,28開方得5。瀏覽器
ccall()相似,不過還要接受其餘參數。下面代碼就是直接用int_sqrt計算28的開方。函數
// Call C from JavaScript var result = Module.ccall('int_sqrt', // name of C function 'number', // return type ['number'], // argument types [28]); // arguments // result is 5
使用 ccall()或者cwrap()的注意事項: * 這些方法用於編譯的C函數,對進行過函數名改編的C++函數不工做。 * 推薦你導出你要用JavaScript調用的函數。 A.導出是在編譯階段作的。好比-s EXPORTED_FUNCTIONS='["_main","_other_function"]' 導出了main()和other_function()。 B.導出時給函數名加下劃線「_」,見A。 C.A中把main也導出了,若是你不導出main,mian就會變成無效代碼,這個導出列表應該是完整 的能夠keepalive的函數列表。 D.Emscripten會作無效代碼清除以減少生成的代碼體積,因此請確保導出了全部你想用js調的函 數。 E.若是編譯是優化編譯-O2級別及以上,會進行代碼改編,包括函數名。可是經過-s EXPORTED_FUNCTIONS導出的函數能夠繼續使用原來的函數名。 F.若是你想導出一個js庫函數(好比,src/library*.js這樣的),除了用EXPORTED_FUNCTIONS ,還得用DEFAULT_LIBRARY_FUNCS_TO_INCLUDE。 * 使用Module.ccall調用,不要直接用ccall。前者在代碼進行的是 優化編譯 的狀況下也工做。
若是你有個C庫,暴露了一些程序/函數。以下:
//api_example.c #include <stdio.h> #include <emscripten.h> EMSCRIPTEN_KEEPALIVE void sayHi() { printf("Hi!\n"); } EMSCRIPTEN_KEEPALIVE int daysInWeek() { return 7; }
使用編譯命令:
emcc api_example.c -o api_example.js
能夠用如下代碼執行這個庫中的函數:
var em_module = require('./api_example.js'); em_module._sayHi(); // direct calling works em_module.ccall("sayHi"); // using ccall etc. also work console.log(em_module._daysInWeek()); // values can be returned, etc.
這就是簡單的編譯C函數和Node的交互。
C中的函數編譯爲js中函數後,其實你能夠直接調的,好比C中有個a(),在編譯後js用_a()來調,不用非要使用ccall()和cwarp(),不過有時候直接調用會稍微複雜點。可能須要你調試一下。
注意上面的_a()中的「_」,直接調的話是通常是要加的。
note: 用ccall()和cwarp()來調,就不用加下劃線。C中是什麼函數名,就js使用什麼函數名,傳給ccall()或cwarp()。
直接調的時候,多多注意函數的參數,確保他們有意義。若是是整型和浮點數原樣傳遞,指針參數在編譯後要按簡單整數來傳。
Pointer_stringify()將C指針轉爲字符串,將字符串轉爲指針,請用 ptr =
allocate(intArrayFromString(someString), 'i8', ALLOC_NORMAL).
還有一些轉換字符串的函數,能夠在preamble.js中找。
Emscripten提供兩種方法讓C/C++調用JavaScript,一種是使用 emscripten_run_script()運行js腳本,一種是寫「內聯JavaScript」。
emscripten_run_script()最直接,但略慢的方式。它是經過eval()來實現的。舉例,在C代碼中插下面一行代碼,未來編譯後就能在瀏覽器彈出alert()。
emscripten_run_script("alert('hi')");
note: 由於alert函數只有瀏覽器中有,node中沒有,因此一個通用的方式是用Module.print().
第二種,用EM_ASM()和其餘相關宏寫內聯JavaScript,這種方式就稍微快一點。使用這種方式實現上面那個alert,代碼就是:
#include <emscripten.h> int main() { EM_ASM( alert('hello world!'); throw 'all done'; ); return 0; }
你也能夠在C中傳值給JavaScript,那就用EM_ASM_(比EM_ASM多了「_」),舉例:
EM_ASM_({ Module.print('I received: ' + $0); }, 100);
輸出:I received: 100。
也能夠有返回值,用EM_ASM_INT。舉例
int x = EM_ASM_INT({ Module.print('I received: ' + $0); return $0 + 1; }, 100); printf("%d\n", x);
返回101。
更多內容參見emscripten.h部分的API文檔。
note: * 若是返回值是int或double,你要指定不一樣宏,是EM_ASM_INT仍是EM_ASM_DOUBLE。 * 輸入參數用$0,$1等形式表示。 * 返回值用於js傳給c數據。 * 好好看一下上面幾段代碼中{}的用法,它用來區分哪裏是參數,哪裏是輸入值。 * 使用 EM_ASM 注意用‘’,不要用「」。不然會有語法錯誤。
在JavaScript中實現C API是有可能的。Emscripten的不少庫,好比SDL1 and OpenGL就用到這個方法。
能夠寫js API 讓C/C++來調用,爲實現它,你要定義接口,用extern來標記它是個外部API。而後默認狀況系,你去library.js裏面實現這個接口。編譯時,編譯器會尋找這些js庫。
默認下,接口實現代碼要寫在library.js裏面。你也可使用編譯選項--js-library把實現接口的代碼放到自定義.js文件中。
舉例:
extern void my_js(void); int main() { my_js(); return 1; }
note: 若是你用的C++,請用extern "C" {}把extern void my_js();括起來。以下: extern "C" { extern void my_js(); }
上面定義了接口,而且還在main裏面調用了。下面就去library.js這個默認庫裏面實現這個接口,代碼以下:
my_js: function() { alert('hi'); },
這樣就至關於在C中調用一個JS庫的API。
使用Runtime.addFunction返回一個整數來表示一個函數指針。把這個整數傳給C代碼,而後C代碼調用那個值,則傳給Runtime.addFunction的JavaScript函數就被調用。
當你用Runtime.addFunction,會有一個數組來存這些函數。這個數組的大小必須被明確指定,這能夠經過編譯設置項RESERVED_FUNCTION_POINTERS來作。舉例,保存20個函數大小。
emcc ... -s RESERVED_FUNCTION_POINTERS=20 ...
你可使用getValue(ptr,type)和setValue(ptr,value,type)訪問內存。第一個參數ptr是一個指針(表明一個內存地址的數字)。類型type必須爲LLVM IR類型,i八、i1六、i3二、i6四、float、double或相似i8 (或只有 )的指針類型。
這是一個比ccall()和cwrap()更底層的操做
您還能夠經過操縱表示內存的數組來直接訪問內存。這是不推薦的,除非您肯定您知道本身在作什麼,而且它比getValue()和setValue()須要更多開銷。
若是您想從JavaScript導入大量數據讓編譯代碼進行處理,那麼可能須要這樣作。例如,下面的代碼分配一個緩衝區,在其中放一些數據,調用C函數來處理數據,最後釋放緩衝區。
var buf = Module._malloc(myTypedArray.length*myTypedArray.BYTES_PER_ELEMENT); Module.HEAPU8.set(myTypedArray, buf); Module.ccall('my_function', 'number', ['number'], [buf]); Module._free(buf);
這裏my_function是一個C函數,它接收一個整數參數(或者一個指針,它們都是32位的整數),並返回一個整數。多是int my_function(char * buf)這樣。
Module是一個全局JavaScript對象,它有不少Emscripten生成的代碼在執行的時候會調用的屬性。
開發者能夠提供Module的實現以控制Emscripten消息通知的顯示行爲,主循環運行前加載哪些文件,以及其餘不少行爲。
有時,編譯代碼須要訪問環境變量(例如,在C中調用getenv()函數)。emscripten生成的JavaScript沒法直接訪問計算機的環境變量,所以提供了一個「虛擬化」的環境。
JavaScript對象ENV包含虛擬化環境變量,經過修改它能夠將變量傳遞給編譯後的代碼。必須注意確保ENV變量在修改前已由Emscripten初始化。Module.preRun能夠作這個。
例如,要設置一個環境變量MY_FILE_ROOT爲「/ usr/lib/test/」,您能夠將如下JavaScript添加到Module設置代碼中:
Module.preRun.push(function() {ENV.MY_FILE_ROOT = "/usr/lib/test"})
Emscripten代碼移植主題系列文章是emscripten中文站點的一部份內容。
第一個主題介紹代碼可移植性與限制
第二個主題介紹Emscripten的運行時環境
第三個主題第一篇文章介紹鏈接C++和JavaScript
第三個主題第二篇文章介紹embind
第四個主題介紹文件和文件系統
第六個主題介紹Emscripten如何調試代碼