Konfiguracja i wątki do WebAssembly

0

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:

  1. 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.
  2. 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).
  3. 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?
0

W chwili obecnej, moim celem WASM jest zrobienie możliwie najprostszego, najkrótszego programu i kodu HTML/JS, który zawiera w sobie następujące elementy:

  1. Wywołanie funkcji przyjmującej liczbę jako argument - cel zrealizowany.
  2. Wywołanie funkcji zwracającej liczbę - cel zrealizowany.
  3. Wywołanie funkcji przyjmującej tekst jako argument - TODO.
  4. Wywołanie funkcji zwracającej tekst - zrobiłem jak w przykładzie, ale nie działa.
  5. Funkcja w C++ wywołuje funkcję Javascript z argumentem liczbowym i odczytuje wynik liczbowy wykonania tej funkcji - Zrealizowane, ale działa inaczej niż w dokumentacji.
  6. Funkcja w C++ wywołuje funkcję Javascript z argumentem tekstowym i odczytuje wynik tekstowy wykonania tej funkcji - TODO.
  7. Wielowątkowość w postaci funkcji pracującej w pętli, a konkretnie przycisk START, STOP i STATUS - nie wiem, jak użyć plików JS lub jak zrobić najprostszy worker.

Jak uda mi się ogarnąć wszystkie wyżej wymienione, to już wiedziałbym, jak przerobić jakiś swój "prawdziwy" program w C++ na HTML/JavaScript/WASM. Takiej bazy to próżno szukać w internecie, dlatego chcę ja sobie utworzyć. Obecnie chcę się skupić na tych celach z pogrubionym komentarzem.

To są podstawy podstaw, a jakość nikt nie potrafi napisać kompletnych przykładów, w którym to wszystko jest. Jak kopiuję przykład z internetu, to zawsze jest jakiś problem.

tumor napisał(a):

Powinieneś użyć web workerów.
https://emscripten.org/docs/api_reference/wasm_workers.html

W C++ umożliwiłem teoretyczne wykorzystanie obu mechanizmów, czyli pthread (odkomentować linie 5 i 6) i worker (odkomentować linie 8 i 9). Z worker da się skompilować takim poleceniem:

emcc hello.cpp --no-entry -o hello.js -sWASM_WORKERS 

Tworzy trzy pliki JS. W nich jest dużo natworzone, a ja chce najprostszy przykład, jak tego użyć, jak napisać worker, żeby mieć to, co ja bym chciał.

Worker w C++ utworzyłem na podstawie tego pliku: https://github.com/emscripten-core/emscripten/blob/main/test/wasm_worker/wasm_worker_and_pthread.c

W międzyczasie próbowałem też ogarnąć wywołanie JavaScript z WASM i to udaje się, ale działa inaczej niż opisane w https://emscripten.org/docs/api_reference/emscripten.h.html#calling-javascript-from-c-c . Według dokumentacji podaje się, którą funkcję JS chce wywołać i ta funkcja powinna się wywołać, a w moim przypadku, ta funkcja musi być podana w obiekcie przekazywanym do WASM i to ona się wywołuje. Niby cel zrealizowany, wartości przechodzą w obie strony, da się to wykorzystać, można odhaczyć, ale dlaczego działa inaczej niż opisane?

Teraz pozostał również temat napisów. Próbowałem dopisać jakąś najprostsza funkcję posiłkując się tym: https://fsgeek.pl/post/webassembly-jak-zaczac/ Ale okazuje się, że nie zwraca tekstu, nawet dopisałem sprawdzenie, czy w pamięci współdzielonej jest cokolwiek i ona jest pusta. Dlaczego? Co ja robię nie tak?

Bez wątków, to kompiluję C++ takim poleceniem, mimo, ze nie wykorzystuje wytworzonego JS, w którym jest tyle natworzone, że dużo czasu trzeba by spędzić, żeby wiedzieć, co jest czym.

emcc hello.cpp  --no-entry -o hello.js

Obecnie tak wygląda kod w C++

#include <iostream> 
#include <emscripten.h>


//#define UseThr
//#include <pthread.h>

//#define UseWrk
//#include <emscripten/wasm_worker.h>


EMSCRIPTEN_KEEPALIVE
const char * StrTest()
{
  return "Hello World";
}




EM_JS(void, two_alerts, (), {
  alert('hai');
  alert('bai');
});

EM_JS(int, take_args, (int x, float y), {
  console.log('I received: ' + [x, y]);
});



EMSCRIPTEN_KEEPALIVE
int CallbackTest(int n)
{
   two_alerts();
   int T = take_args(100, 35.5);

   emscripten_run_script("alert('hi')");
   
   return T;
}

EMSCRIPTEN_KEEPALIVE
int fib(int x)
{
  if (x < 1)
    return 0;
  if (x == 1)
    return 1;
  return fib(x-1)+fib(x-2);
}


int ThrCounter;
bool ThrWork;

#ifdef UseThr
    pthread_t Thr;
#endif
#ifdef UseWrk
    emscripten_wasm_worker_t Wrk;
#endif

void ThrProc()
{
    ThrCounter = 0;
    ThrWork = true;
    while (ThrWork)
    {
        ThrCounter++;
    }
}

void * ThrProc0(void *arg)
{
    ThrProc();
    return 0;
}

EMSCRIPTEN_KEEPALIVE
int ThrStart()
{
    int result = 0;
    #ifdef UseThr
        result = pthread_create(&Thr, NULL, ThrProc0, (void *)NULL);
    #endif
    #ifdef UseWrk
        Wrk = emscripten_malloc_wasm_worker(/*stack size: */1024);
        emscripten_wasm_worker_post_function_v(Wrk, ThrProc);
    #endif
    return result;
}

EMSCRIPTEN_KEEPALIVE
int ThrStop()
{
    ThrWork = false;
    #ifdef UseThr
        int result = pthread_join(Thr, NULL);
    #endif
    return 0;
}

EMSCRIPTEN_KEEPALIVE
int ThrStatus()
{
    return ThrCounter;
}

A tak odpowiadający mu kod HTML/JS:

<!doctype html> 
<html>
   <head> 
      <meta charset="utf-8">
      <title>WebAssembly test</title>
      <style>
         div { border-style:solid; } 
      </style>
   </head>
   <body>
      <a href="/web/index.html">XXX</a>
      <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> 
      
         // Modul WebAssembly
         let WasmModule;

         // 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 ImportObj = {
              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'
                }),
                two_alerts: () => {
                  Info("ImportObj: Funkcja two_alerts");
                  return 456;
                },
                take_args: (arg1, arg2) => {
                  Info("ImportObj: Funkcja take_args: " + arg1 + "___" + arg2);
                  return 456;
                },
                emscripten_run_script: arg => {
                  Info("ImportObj: Funkcja emscripten_run_script: " + arg);
                  return 0;
                }
              }
            }


         // Wywolanie zwrotne z kodu C++ w WASM
         function WasmCallback(arg)
         {
            Info("Callback: " + arg);
            return 0;
         }

         
         // Uruchamianie prostych funkcji dla sprawdzenia, czy w ogole działa
         function Test()
         {
            // Wywolanie funkcji przyjmujacej liczbe i zwracajace liczbe - DZIAŁA
            Info("fib(6)=" + WasmInstance.exports._Z3fibi(6));
            
            // Testowanie wywolania funkcji JavaScript - DZIAŁA, ale inaczej niżw dokumentacji
            Info("Callback test start");
            Info("Javascript callback: " + WasmInstance.exports._Z12CallbackTesti(123));
            Info("Callback test stop");
            
            // Wywolanie funkcji zwracajacej tekst - wedlug https://fsgeek.pl/post/webassembly-jak-zaczac/
            Info("String test start");
            let i = WasmInstance.exports._Z7StrTestv();

            // Uzyskanie zwroconego tekstu - NIE MOŻNA UZYSKAĆ TEKSTU
            const memoryArr = new Uint8Array(ImportObj.env.memory.buffer);
            Info("Memory length: " + memoryArr.length);
            let response = '';
            while(memoryArr[i]){
              response = response + String.fromCharCode( memoryArr[i] );
              i++;
            }
            
            // Wypisanie tekstu
            Info("StrTest()=" + response);
            
            // Wypisanie calej pamieci w celu testowym - PAMIĘĆ JEST PUSTA
            response = '';
            let N = 0;
            i = 0;
            while(i < memoryArr.length){
              if (memoryArr[i]) { response = response + memoryArr[i]; N++; }
              i++;
            }
            Info("Whole memory: [" + response + "] " + N);
            Info("String test stop");
         }
         
         
         function ThrStart()
         {
            Info("Wątek start Thr " + WasmInstance.exports._Z8ThrStartv());
         }
         function ThrStop()
         {
            Info("Wątek stop Thr " + WasmInstance.exports._Z7ThrStopv());
         }
         function ThrStatus()
         {
            Info("Wątek status Thr " + WasmInstance.exports._Z9ThrStatusv());
         }
         
         
         // 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 => {
                WasmModule = module;
                console.log("Tworzenie instancji");
                let AsmModInst = new WebAssembly.Instance(module, ImportObj);
                console.log("Instancja utworzona");
                return AsmModInst;
              })
            .then(instance => {
                WasmInstance = instance;

                Info("#### WasmInstance.exports");
                Info(getMethods(WasmInstance.exports).join("<br>"));
                Info("####");
         });
      </script>
   </body>
</html>
0

Aby móc łatwiej prowadzić dyskusję, założyłem roboczy projekt na githubie:

https://github.com/andrzejlisek/WebAssemblyBase

Ten projekt online: https://andrzejlisek.github.io/WebAssemblyBase/

Myślę, że łatwiej będzie aktualizować projekt niż wklejać całe źródło na forum.

Jak kompiluję za pomocą emcc hello.cpp -Os -s WASM=1 -s SIDE_MODULE=1 -o hello.wasm to tym razem udaje się wyciągnąć tekst z funkcji zwracającej const char *. Natomiast, w jaki sposób należy wprowadzić słowo do funkcji przyjmującej const char * jako argument? Jak w kodzie C++ użyję malloc lub free, to już instancja w JavaScript się nie tworzy.

Ogólnie, to szukam najbardziej optymalnych parametrów kompilacji tak, żeby kompilował i uruchamiał się dowolny kod C++, taki, że kod C++ zawiera samą logikę biznesową, a w HTML/JavaScript jest GUI i IO.

0

Wysłałem aktualizacje projektu.
https://github.com/andrzejlisek/WebAssemblyBase
Ten projekt online: https://andrzejlisek.github.io/WebAssemblyBase/
Projekt nie działa online z tego powodu, że nie jest możliwe wymuszenie działania SharedArrayBuffer wymagane do wątków. W przypadku PHP wystarczy dodać taki kod:

<?php
header("Cross-Origin-Embedder-Policy: require-corp");
header("Cross-Origin-Opener-Policy: same-origin");
?>

Natomiast do Github pages nie można zastosować żadnego języka serwerowego, sprawdziłem https://github.com/orgs/community/discussions/13309 , ale to też nie działa.

Okazuje się, że żeby skompilować kod bez wątków, najlepsze jest takie wywołanie:
emcc hello.cpp -o hello.js -s NO_EXIT_RUNTIME=1 -s "EXPORTED_RUNTIME_METHODS=['ccall']"
Plików JS nie trzeba modyfikować, początkowo trzeba dać -o hello.html i usunąć z tego pliku wszystko co nie potrzebne.

Oprócz wątków udało się zrealizować wszystko, co chciałem, a same wątki próbuję z użyciem WASM Worker. Oczywiście, żeby wątki teoretycznie działały, kompiluje takim poleceniem:
emcc hello.cpp -o hello.js -s NO_EXIT_RUNTIME=1 -s "EXPORTED_RUNTIME_METHODS=['ccall']" -sWASM_WORKERS

Teraz problem jest taki, że wątek nie uruchamia się wcale.

Nawet, jak zrobię przykład, który jest w Quick Example w https://emscripten.org/docs/api_reference/wasm_workers.html to też nie działa. Program kompiluje się, uruchamia, ale nie uruchamia wątku. Ten przykład kompilowałem poleceniem emcc test.cpp -o test.html -s NO_EXIT_RUNTIME=1 -s "EXPORTED_RUNTIME_METHODS=['ccall']" -sWASM_WORKERS, nic nie zmieniałem w wytworzonym HTML, tylko dopisałem powyższy kod PHP, zmieniłem rozszerzenie z html na php, uruchomiłem na lokalnym serwerze obsługującym PHP, a i tak nie działa.

Gdzie może być problem i jak zmusić wątek do uruchomienia?

W chwili uruchomienia wątku nie jest rzucany żaden wyjątek, po prostu funkcja przekazywana do wątku nie startuje.

0

Nikt nie wie, ale udało mi się z tym poradzić i zaktualizowałem projekt na Github. Jednak należy wiedzieć, że funkcja w wątku nie może używać wywołań Javascript, a uruchomienie jest możliwe tylko w przypadku, gdy przeglądarka ma włączone SharedArrayBuffer.

1 użytkowników online, w tym zalogowanych: 0, gości: 1