Właśnie wchodzę w nowy dla mnie temat, jakim jest WebAssembly. Umiem programować w C++, C#, Java, w ogóle nie znam Rust, dlatego wybór padł na Emscripten. Wydaje się to być najprostszym narzędziem.
Mam bardzo prosty przykład, konkretnie dwie funkcje jednoargumentowe zwracające liczbę i możliwie najprostszy przykład pracy "w tle", w którym uruchamia się pracę w pętli, można podejrzeć jej status i zakończyć pracę (warunek iteracji przestaje być spełniony).
Po wielu próbach doszedłem do czegoś takiego:
Plik hello.cpp:
#include <iostream>
#include <emscripten.h>
#include <pthread.h>
EMSCRIPTEN_KEEPALIVE
int reversenumber(int n)
{
int reverse=0, rem;
while(n!=0)
{
rem=n%10; reverse=reverse*10+rem; n/=10;
}
return reverse;
}
EMSCRIPTEN_KEEPALIVE
int fib(int x)
{
if (x < 1)
return 0;
if (x == 1)
return 1;
return fib(x-1)+fib(x-2);
}
pthread_t Thr;
int ThrCounter;
bool ThrWork;
void * ThrProc(void *arg)
{
ThrCounter = 0;
ThrWork = true;
while (ThrWork)
{
ThrCounter++;
}
return 0;
}
EMSCRIPTEN_KEEPALIVE
int ThrStart(int dummy)
{
int result = pthread_create(&Thr, NULL, ThrProcmodule, (void *)NULL);
return result;
}
EMSCRIPTEN_KEEPALIVE
int ThrStop(int dummy)
{
ThrWork = false;
int result = pthread_join(Thr, NULL);
return result;
}
EMSCRIPTEN_KEEPALIVE
int ThrStatus(int dummy)
{
return ThrCounter;
}
Plik index.html:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>WebAssembly test</title>
<style>
div { border-style:solid; }
</style>
</head>
<body>
<div id="textcontent"></div>
<input type="button" value="Test" onclick="Test()">
<input type="button" value="Start" onclick="ThrStart()">
<input type="button" value="Stop" onclick="ThrStop()">
<input type="button" value="Status" onclick="ThrStatus()">
<script>
// Instancja WebAssembly
let WasmInstance;
// Obiekt przekazywany do funkcji tworzacej instancje
// https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/Memory
// potrzebny w przypadku, gdy w C++ jest tworzenie tablic, obiektów.
const sp = {
wasi_snapshot_preview1: {
proc_exit: arg => {
console.log("Funkcja proc_exit");
console.log(arg);
},
clock_time_get: arg => {
console.log("Funkcja clock_time_get");
return performance.now();
}
},
env: {
memoryBase: 0,
tableBase: 0,
memory: new WebAssembly.Memory({
initial: 256
}),
table: new WebAssembly.Table({
initial: 0,
element: 'anyfunc'
})
}
}
// Uruchamianie prostych funkcji dla sprawdzenia, czy w ogole działa
function Test()
{
Info("reversenumber(1439898)=" + WasmInstance.exports._Z13reversenumberi(1439898));
Info("fib(6)=" + WasmInstance.exports._Z3fibi(6));
}
function ThrStart()
{
Info("Wątek start " + WasmInstance.exports._Z8ThrStarti(1));
}
function ThrStop()
{
Info("Wątek stop " + WasmInstance.exports._Z7ThrStopi(1));
}
function ThrStatus()
{
Info("Wątek status " + WasmInstance.exports._Z9ThrStatusi(1));
}
// Wypisanie tekstu informacyjnego na stronie poprzez dopisanie tekstu do div
function Info(Msg)
{
document.getElementById("textcontent").innerHTML = document.getElementById("textcontent").innerHTML + Msg + "<br>";
}
// Tworzenie listy elementów obiektu - https://flaviocopes.com/how-to-list-object-methods-javascript/
function getMethods(obj)
{
let properties = new Set()
let currentObj = obj
do {
Object.getOwnPropertyNames(currentObj).map(item => properties.add("[" + typeof obj[item] + "] " + item));
} while ((currentObj = Object.getPrototypeOf(currentObj)));
let ItemList = [...properties.keys()];
return ItemList;
}
console.log("start");
fetch("hello.wasm")
.then(bytes => bytes.arrayBuffer())
.then(mod => WebAssembly.compile(mod))
.then(module => {
console.log("Tworzenie instancji");
let AsmModInst = new WebAssembly.Instance(module, sp);
console.log("Instancja utworzona");
return AsmModInst;
})
.then(instance => {
WasmInstance = instance;
Info("#### WasmInstance.exports");
Info(getMethods(WasmInstance.exports).join("<br>"));
Info("####");
});
</script>
</body>
</html>
Plik hello.cpp kompiluję takim poleceniem:
emcc hello.cpp --no-entry -s STANDALONE_WASM -o hello.wasm
Funkcje liczbowe, czyli reversenumber
i fib
działają poprawnie, czyli wywołuję funkcję i otrzymuję wynik.
Natomiast wątek nie tworzy się i nie działa.
Wyczytałem, że z wątkiem należy dodać parametr -pthread
, wiec kompiluję takim poleceniem:
emcc hello.cpp --no-entry -s STANDALONE_WASM -o hello.wasm -pthread
Jednak po tej poprawce, w chwili tworzenia się instancji, pojawia się taki błąd: import object field 'emscripten_check_blocking_allowed' is not a Function
. Nie wiem, gdzie tą funkcję dopisać. Wcześniej były podobne błędy, ale udało się je rozwiązać dopisując elementy do zmiennej sp
.
Pytania i problemy:
- Moje wypociny bazują na https://www.tutorialspoint.com/webassembly/webassembly_working_with_cplusplus.htm i na https://medium.com/@tdeniffel/pragmatic-compiling-from-c-to-webassembly-a-guide-a496cc5954b8 . Od razu to miałem problem z uruchomieniem, ale z trudem się udało. Czy tak to właśnie powinno wyglądać? Chodzi o rzecz możliwie najprostszą, czyli w C++ pisze jakiś moduł, który właściwie może być całym programem, tylko bez funkcji
int main()
, a z JavaScript wywołuję różne funkcje. - W jaki sposób zmusić wątki do działania? Chodzi o możliwie najprostszy przykład. W załączonym pliku cpp, takie uruchomienie wątków na pewno by zadziałało pisząc zwykłą aplikacje w C++, która ma trzy przyciski (start, stop, status).
- Czy parametr
-s STANDALONE_WASM
jest przydatny, czy szkodliwy. Pomijając wątki to obojętnie czy jest, czy go nie ma, to i tak się kompiluje i uruchamia. Po co w niektórych przypadkach zaleca się-s STANDALONE_WASM
?