Merge remote-tracking branch 'origin/main' into threadutil
diff --git a/ChangeLog.md b/ChangeLog.md index d4ca7d1..cd3ac1e 100644 --- a/ChangeLog.md +++ b/ChangeLog.md
@@ -20,6 +20,8 @@ 2.0.30 ------ +- Add `emscripten/thread_utils.h` helper header, which includes C++ utilities + for proxying code to other threads. 2.0.29 ----- - Bug fixes
diff --git a/site/source/docs/porting/pthreads.rst b/site/source/docs/porting/pthreads.rst index 422ab88..46b9a3e 100644 --- a/site/source/docs/porting/pthreads.rst +++ b/site/source/docs/porting/pthreads.rst
@@ -148,6 +148,16 @@ Also note that when compiling code that uses pthreads, an additional JavaScript file ``NAME.worker.js`` is generated alongside the output .js file (where ``NAME`` is the basename of the main file being emitted). That file must be deployed with the rest of the generated code files. By default, ``NAME.worker.js`` will be loaded relative to the main HTML page URL. If it is desirable to load the file from a different location e.g. in a CDN environment, then one can define the ``Module.locateFile(filename)`` function in the main HTML ``Module`` object to return the URL of the target location of the ``NAME.worker.js`` entry point. If this function is not defined in ``Module``, then the default location relative to the main HTML file is used. +C++ helpers +=========== + +High-level C++ helper code is available by including +``emscripten/thread_utils.h``. See example code in the relevant tests: + +* `Async proxying to the main thread <https://github.com/emscripten-core/emscripten/tree/main/tests/core/pthread/invoke_on_main_thread.cpp>`_. +* `Blocking while running async code on a pthread <https://github.com/emscripten-core/emscripten/tree/main/tests/core/pthread/sync_to_async.cpp>`_. + + Running code and tests ======================
diff --git a/system/include/emscripten/thread_utils.h b/system/include/emscripten/thread_utils.h new file mode 100644 index 0000000..81826d4 --- /dev/null +++ b/system/include/emscripten/thread_utils.h
@@ -0,0 +1,194 @@ +/* + * Copyright 2021 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + */ + +#pragma once + +#include <assert.h> +#include <emscripten.h> +#include <emscripten/threading.h> +#include <pthread.h> + +#include <functional> +#include <thread> +#include <utility> + +namespace emscripten { + +// Helper function to call a callable object on the main thread in a fully +// asynchronous manner. +// +// Normal proxying to the main thread runs into the possible issue of the main +// thread pumping the event queue while it blocks. That is necessary for e.g. +// WebGL proxying where both sync and async events must be received and run in +// order. With this class, only async events are allowed, and they are run in +// an async manner on the main thread, that is, with nothing else on the stack +// while they run (even if this event blocks on something, which causes the +// main event loop to run events, if another event like this arrives, it would +// be queued to run later, avoiding a collision). + +template <class F> +void invoke_on_main_thread_async(F&& f) { + using function_type = typename std::remove_reference<F>::type; + + auto run_on_main_thread = [](void* f1) { + // Once on the main thread, run as an event from the JS event queue + // directly, so nothing else is on the stack when we execute. This means + // we are no longer ordered with respect to synchronous proxied calls, + // but that is ok in this case as all we care about are async ones. + emscripten_async_call( + [](void* f2) { + auto f = static_cast<function_type*>(f2); + (*f)(); + delete f; + }, + f1, + 0 + ); + }; + + // Proxy the call to the main thread. + emscripten_async_run_in_main_runtime_thread( + EM_FUNC_SIG_VI, + static_cast<void (*)(void*)>(run_on_main_thread), + // If we were passed in something we need to copy, copy it; we end up with + // a new allocation here on the heap that we will free later. + new function_type(std::forward<F>(f)) + ); +} + +// Helper class for generic sync-to-async conversion. Creating an instance of +// this class will spin up a pthread. You can then call invoke() to run code +// on that pthread. The work done on the pthread receives a callback method +// which lets you indicate when it finished working. The call to invoke() is +// synchronous, while the work done on the other thread can be asynchronous, +// which allows bridging async JS APIs to sync C++ code. +// +// This can be useful if you are in a location where blocking is possible (like +// a thread, or when using PROXY_TO_PTHREAD), but you have code that is hard to +// refactor to be async, but that requires some async operation (like waiting +// for a JS event). +class sync_to_async { + +// Public API +//============================================================================== +public: + + // Pass around the callback as a pointer to a std::function. Using a pointer + // means that it can be sent easily to JS, as a void* parameter to a C API, + // etc., and also means we do not need to worry about the lifetime of the + // std::function in user code. + using Callback = std::function<void()>*; + + // + // Run some work on thread. This is a synchronous (blocking) call. The thread + // where the work actually runs can do async work for us - all it needs to do + // is call the given callback function when it is done. + // + // Note that you need to call the callback even if you are not async, as the + // code here does not know if you are async or not. For example, + // + // instance.invoke([](emscripten::sync_to_async::Callback resume) { + // std::cout << "Hello from sync C++ on the pthread\n"; + // (*resume)(); + // }); + // + // In the async case, you would call resume() at some later time. + // + void invoke(std::function<void(Callback)> newWork); + +//============================================================================== +// End Public API + +private: + std::unique_ptr<std::thread> thread; + std::mutex mutex; + std::condition_variable condition; + std::function<void(Callback)> work; + bool readyToWork = false; + bool finishedWork; + bool quit = false; + std::unique_ptr<std::function<void()>> resume; + + // The child will be asynchronous, and therefore we cannot rely on RAII to + // unlock for us, we must do it manually. + std::unique_lock<std::mutex> childLock; + + static void* threadMain(void* arg) { + emscripten_async_call(threadIter, arg, 0); + return 0; + } + + static void threadIter(void* arg) { + auto* parent = (sync_to_async*)arg; + if (parent->quit) { + pthread_exit(0); + } + // Wait until we get something to do. + parent->childLock.lock(); + parent->condition.wait(parent->childLock, [&]() { + return parent->readyToWork; + }); + auto work = parent->work; + parent->readyToWork = false; + // Allocate a resume function, and stash it on the parent. + parent->resume = std::make_unique<std::function<void()>>([parent, arg]() { + // We are called, so the work was finished. Notify the caller. + parent->finishedWork = true; + parent->childLock.unlock(); + parent->condition.notify_one(); + // Look for more work. Doing this asynchronously ensures that we continue + // after the current call stack unwinds (avoiding constantly adding to the + // stack, and also running any remaining code the caller had, like + // destructors). TODO: add an option to do a synchronous call here in some + // cases, which would avoid the time delay caused by a browser setTimeout. + emscripten_async_call(threadIter, arg, 0); + }); + // Run the work function the user gave us. Give it a pointer to the resume + // function. + work(parent->resume.get()); + } + +public: + sync_to_async() : childLock(mutex) { + // The child lock is associated with the mutex, which takes the lock as we + // connect them, and so we must free it here so that the child can use it. + // Only the child will lock/unlock it from now on. + childLock.unlock(); + + // Create the thread after the lock is ready. + thread = std::make_unique<std::thread>(threadMain, this); + } + + ~sync_to_async() { + // Wake up the child to tell it to quit. + invoke([&](Callback func){ + quit = true; + (*func)(); + }); + + thread->join(); + } +}; + +void sync_to_async::invoke(std::function<void(Callback)> newWork) { + // Send the work over. + { + std::lock_guard<std::mutex> lock(mutex); + work = newWork; + finishedWork = false; + readyToWork = true; + } + condition.notify_one(); + + // Wait for it to be complete. + std::unique_lock<std::mutex> lock(mutex); + condition.wait(lock, [&]() { + return finishedWork; + }); +} + +} // namespace emscripten
diff --git a/tests/core/pthread/invoke_on_main_thread.cpp b/tests/core/pthread/invoke_on_main_thread.cpp new file mode 100644 index 0000000..f5eb43e --- /dev/null +++ b/tests/core/pthread/invoke_on_main_thread.cpp
@@ -0,0 +1,40 @@ +#include <iostream> + +#include <emscripten.h> + +#include <emscripten/thread_utils.h> + +int main() { + struct Foo { + void operator()() { + // Print whether we are on the main thread. + auto mainThread = EM_ASM_INT({ + var mainThread = typeof importScripts === 'undefined'; + console.log("hello. mainThread=", mainThread); + return mainThread; + }); + + // If we are on the main thread, it is time to end this test. Do so + // asynchronously, as if we exit right now then the object Foo() we are + // called on will not yet be destroyed, which causes a false positive in + // LSan leak detection. + if (mainThread) { + emscripten_async_call( + [](void*) { + exit(0); + }, + nullptr, + 0 + ); + } + } + }; + + // Call it on this thread. + Foo()(); + + // Call it on the main thread. + emscripten::invoke_on_main_thread_async(Foo()); + + emscripten_exit_with_live_runtime(); +}
diff --git a/tests/core/pthread/invoke_on_main_thread.out b/tests/core/pthread/invoke_on_main_thread.out new file mode 100644 index 0000000..c359722 --- /dev/null +++ b/tests/core/pthread/invoke_on_main_thread.out
@@ -0,0 +1,2 @@ +hello. mainThread= false +hello. mainThread= true
diff --git a/tests/core/pthread/sync_to_async.cpp b/tests/core/pthread/sync_to_async.cpp new file mode 100644 index 0000000..ca1cfbd --- /dev/null +++ b/tests/core/pthread/sync_to_async.cpp
@@ -0,0 +1,53 @@ +#include <iostream> + +#include <emscripten/thread_utils.h> + +int main() { + emscripten::sync_to_async sync_to_async; + + std::cout << "Perform a synchronous task.\n"; + + sync_to_async.invoke([](emscripten::sync_to_async::Callback resume) { + std::cout << " Hello from sync C++\n"; + (*resume)(); + }); + + std::cout << "Perform an async task.\n"; + + sync_to_async.invoke([](emscripten::sync_to_async::Callback resume) { + std::cout << " Hello from sync C++ before the async\n"; + + // Set up async JS, just to prove an async JS callback happens before the + // async C++. + EM_ASM({ + setTimeout(function() { + console.log(" Hello from async JS"); + }, 0); + }); + + // Set up async C++.. + emscripten_async_call([](void* arg) { + auto resume = (emscripten::sync_to_async::Callback)arg; + std::cout << " Hello from async C++\n"; + + // We are done with all the async things we want to do, and can call + // resume to continue execution on the calling thread. + (*resume)(); + }, resume, 1); + }); + + std::cout << "Perform another synchronous task, also showing var capture.\n"; + + int var = 41; + + sync_to_async.invoke([&](emscripten::sync_to_async::Callback resume) { + std::cout << " Hello again from sync C++, we captured " << var << '\n'; + var++; + (*resume)(); + }); + + std::cout << "Captured var is now " << var << '\n'; + assert(var == 42); + + return 0; +}
diff --git a/tests/core/pthread/sync_to_async.out b/tests/core/pthread/sync_to_async.out new file mode 100644 index 0000000..07b9bad --- /dev/null +++ b/tests/core/pthread/sync_to_async.out
@@ -0,0 +1,9 @@ +Perform a synchronous task. + Hello from sync C++ +Perform an async task. + Hello from sync C++ before the async + Hello from async JS + Hello from async C++ +Perform another synchronous task, also showing var capture. + Hello again from sync C++, we captured 41 +Captured var is now 42
diff --git a/tests/test_core.py b/tests/test_core.py index 08f744f..5d2cc12 100644 --- a/tests/test_core.py +++ b/tests/test_core.py
@@ -8373,6 +8373,22 @@ self.emcc_args += ['-DDEBUG'] self.do_runf(test_file('core/test_return_address.c'), 'passed') + @node_pthreads + @no_wasm2js('wasm2js does not support PROXY_TO_PTHREAD (custom section support)') + def test_pthread_sync_to_async(self): + self.set_setting('PROXY_TO_PTHREAD') + self.set_setting('EXIT_RUNTIME') + self.do_run_in_out_file_test('core/pthread/sync_to_async.cpp') + + @node_pthreads + @no_wasm2js('wasm2js does not support PROXY_TO_PTHREAD (custom section support)') + def test_pthread_invoke_on_main_thread(self): + self.set_setting('PROXY_TO_PTHREAD') + self.set_setting('EXIT_RUNTIME') + # increase memory for ASan to not hit "internal allocator is out of memory" + self.set_setting('INITIAL_MEMORY', '32MB') + self.do_run_in_out_file_test('core/pthread/invoke_on_main_thread.cpp') + def test_emscripten_atomics_stub(self): self.do_run_in_out_file_test('core/pthread/emscripten_atomics.c')