Emscripten教程之連接C++和JavaScript(三)


本文是Emscripten-WebAssembly專欄系列文章之一,更多文章請查看專欄。
也可以去作者的博客閱讀文章。
歡迎加入Wasm和emscripten技術交流群,群聊號碼:939206522。

Emscripten提供了多種方法來連接和交互JavaScript和編譯的C或c++,本文逐一介紹。

JavaScript使用ccall/cwrap調用編譯的c函數

JavaScript中調用編譯的C函數的最簡單的方法是使用ccall()和cwrap()。

ccall()用具體的參數和返回值調用一個編譯的C函數,而cwrap()是一個編譯的C函數的包裹,調用它會返回一個JavaScript可以調用的函數。如果你打算多次調用一個函數的話,cwrap()用處更大。

舉個例子, 下面代碼是tests/hello_function.cpp文件。int_sqrt()函數外面套了個extern "C"是為了防止C++對它的名字改編。

    #include <math.h> extern "C" { int int_sqrt(int x) { return sqrt(x); } }

使用的編譯命令為:

./emcc tests/hello_function.cpp -o function.html -s EXPORTED_FUNCTIONS="['_int_sqrt']"

編譯完,你可以使用JavaScript調用cwrap()拿到int_sqrt函數。繼而可以進行其他操作。

    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。前者在代碼進行的是 優化編譯 的情況下也工作。

Nodejs與C/C++ API交互

如果你有個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的交互。

JavaScript“直接”調用編譯的C/C++代碼

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中找。

C/C++調用JavaScript

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

在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。

JavaScript對庫文件的限制(todo)

調用JavaScript函數作為C中的函數指針

使用Runtime.addFunction返回一個整數來表示一個函數指針。把這個整數傳給C代碼,然后C代碼調用那個值,則傳給Runtime.addFunction的JavaScript函數就被調用。

當你用Runtime.addFunction,會有一個數組來存這些函數。這個數組的大小必須被明確指定,這可以通過編譯設置項RESERVED_FUNCTION_POINTERS來做。舉例,保存20個函數大小。

emcc ... -s RESERVED_FUNCTION_POINTERS=20 ...

JavaScript進行內存訪問

你可以使用getValue(ptr,type)和setValue(ptr,value,type)訪問內存。第一個參數ptr是一個指針(代表一個內存地址的數字)。類型type必須為LLVM IR類型,i8、i16、i32、i64、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"})

C++和JavaScript綁定---WebIDL Binder和Embind(todo)


Emscripten代碼移植系列文章

Emscripten代碼移植主題系列文章是emscripten中文站點的一部分內容。
第一個主題介紹代碼可移植性與限制
第二個主題介紹Emscripten的運行時環境
第三個主題第一篇文章介紹連接C++和JavaScript
第三個主題第二篇文章介紹embind
第四個主題介紹文件和文件系統
第六個主題介紹Emscripten如何調試代碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM