emscripten-core / emscripten

Emscripten: An LLVM-to-WebAssembly Compiler
Other
25.84k stars 3.31k forks source link

Unexpected error of proxyAsync with emscripten_init_websocket_to_posix_socket_bridge #21744

Open btkhoi2001 opened 7 months ago

btkhoi2001 commented 7 months ago

Please include the following in your bug report:

Version of emscripten/emsdk:

emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 3.1.56 (cf90417346b78455089e64eb909d71d091ecc055) clang version 19.0.0git (https:/github.com/llvm/llvm-project 34ba90745fa55777436a2429a51a3799c83c6d4c) Target: wasm32-unknown-emscripten Thread model: posix

Full link command

EMCC = emcc
CFLAGS = -O3 -std=c++17
EMFLAGS = -lwebsocket.js -sPROXY_POSIX_SOCKETS -pthread -sPROXY_TO_PTHREAD -sUSE_PTHREADS=1 -sPTHREAD_POOL_SIZE=2 -sNO_EXIT_RUNTIME=1 -sMODULARIZE -sEXPORT_NAME="WASMModule" -sEXPORTED_RUNTIME_METHODS=stringToNewUTF8

main:
    $(EMCC) $(CFLAGS) $(EMFLAGS) tcp_echo_client.cpp

I am trying to port my socket application to WebAssembly which uses Full POSIX Sockets over WebSocket Proxy Server. I want to initialize the bridge only in certain case, not when the main function is loaded. So I wrap it in a function with EMSCRIPTEN_KEEPALIVE and call it in the javascript module. I made some changes based on this example test code tcp_echo_client.c

// TCP client that sends a few messages to a server and prints out the replies
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <netdb.h>

#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/websocket.h>
#include <emscripten/threading.h>
#include <emscripten/posix_socket.h>
#include <emscripten/proxying.h>

static EMSCRIPTEN_WEBSOCKET_T bridgeSocket = 0;
static pthread_t mainApplicationThreadId = 0;
emscripten::ProxyingQueue queue;

extern "C" {
EMSCRIPTEN_KEEPALIVE
void initBridgeSocket(const char *bridgeUrl) {
  queue.proxyAsync(mainApplicationThreadId, [&]() {
    printf("bridegeUrl = %s\n", bridgeUrl);
    bridgeSocket = emscripten_init_websocket_to_posix_socket_bridge(bridgeUrl);

    // Synchronously wait until connection has been established.
    uint16_t readyState = 0;
    do {
      emscripten_websocket_get_ready_state(bridgeSocket, &readyState);
      emscripten_thread_sleep(100);
    } while (readyState == 0);

    printf("Bridge socket ready\n");
  });

  queue.execute();
}
} // extern "C"
#endif

int main(int argc , char *argv[]) {
  #ifdef __EMSCRIPTEN__
    mainApplicationThreadId = pthread_self();
    emscripten_exit_with_live_runtime();
  #endif

  return 0;
}

Then initialize a bridge when a user clicks the button.

<body>
    <input type="text" placeholder="ws://localhost:8080" value="ws://localhost:8080" id="a">
    <button onclick="foo()">Trigger</button>

    <script src="a.out.js"></script>
    <script>
        var module;

        document.addEventListener("DOMContentLoaded", async () => {
            module = await WASMModule();
        })

        function foo() {
            const bridgeUrl = document.getElementById("a").value;
            module._initBridgeSocket(module.stringToNewUTF8(bridgeUrl));
        }
    </script>
</body>

Problem: First, the task is still executed even without this line:

  queue.execute();

Second, I want to make sure the bridgeUrl successfully passed into the lambda expression, so I use printf to check it. When I place it before emscripten_init_websocket_to_posix_socket_bridge, It still prints the correct URL, but it gets "flushed" and becomes empty when passing into emscripten_init_websocket_to_posix_socket_bridge

a.out.js:8 Uncaught (in promise) DOMException: Failed to construct 'WebSocket': The URL '' is invalid.
    at _emscripten_websocket_new (http://127.0.0.1:3000/websocket/a.out.js:8:33075)
    at __emscripten_receive_on_main_thread_js (http://127.0.0.1:3000/websocket/a.out.js:8:19685)
    at http://127.0.0.1:3000/websocket/a.out.wasm:wasm-function[103]:0x5620
    at http://127.0.0.1:3000/websocket/a.out.wasm:wasm-function[105]:0x5bd1
    at http://127.0.0.1:3000/websocket/a.out.wasm:wasm-function[107]:0x5cd7
    at http://127.0.0.1:3000/websocket/a.out.wasm:wasm-function[56]:0x2fe4
    at http://127.0.0.1:3000/websocket/a.out.wasm:wasm-function[110]:0x5d6d
    at http://127.0.0.1:3000/websocket/a.out.wasm:wasm-function[56]:0x2fe4
    at http://127.0.0.1:3000/websocket/a.out.wasm:wasm-function[94]:0x524d
    at callUserCallback (http://127.0.0.1:3000/websocket/a.out.js:8:18393)

The same error happens when I change the order, I place printf after. It successfully initiated the bridge, but printf prints empty URL. bridgeUrl is messed up whenever passed into a function.

sbc100 commented 7 months ago

That seems very odd. Can you uncommenting the POSIX_SOCKET_DEBUG line in system/lib/websocket/websocket_to_posix_socket.c and then rebuilding the library using ./embuilder build libsockets_proxy-mt --force

sbc100 commented 7 months ago

Is this something to do with the way the C++ lambda capture works for the bridgeUrl pointer? I would have though it would capture by simply copying the pointer.. but maybe not? @tlively ?

tlively commented 7 months ago

The lambda here is capturing bridgeURL by reference because of the [&] binder. Since the proxying is async, the function returns on the calling thread before the lambda is executed on the thread it was sent to, so when the lambda is eventually executed, the reference to bridgeURL is dangling.

To fix it, you could either make the lambda capture the argument by value or you can switch to synchronous proxying to keep the parameters and local variables on the calling thread alive until the work is complete.

You also don't need the queue.execute() call on the calling thread, since it isn't performing any proxied work. The work will automatically execute on the worker thread as soon as it returns to its event loop.