Commit 152bd6c0 authored by Brateau Etienne's avatar Brateau Etienne Committed by Jean-Baptiste Kempf
Browse files

Apply Emscripten PR #5531 : Multithreading 6/N: Proxy calls to JavaScript...

Apply Emscripten PR #5531 : Multithreading 6/N: Proxy calls to JavaScript functions from pthreads to main browser thread
parent 537b2556
From 46b90cec6ca808fc074c1b79555029f473982b72 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= <jujjyl@gmail.com>
Date: Sat, 26 Aug 2017 15:13:56 +0300
Subject: [PATCH 02/23] Implement a model for synchronously or asynchronously
proxying JS function calls to main browser thread.
---
src/jsifier.js | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 109 insertions(+), 2 deletions(-)
diff --git a/src/jsifier.js b/src/jsifier.js
index 1e4f1191e..40d42e30f 100644
--- a/src/jsifier.js
+++ b/src/jsifier.js
@@ -16,6 +16,15 @@ var INDENTATION = ' ';
var functionStubSigs = {};
+// Some JS-implemented library functions are proxied to be called on the main browser thread, if the Emscripten runtime is executing in a Web Worker.
+// Each such proxied function is identified via an ordinal number (this is not the same namespace as function pointers in general).
+var proxiedFunctionTable = ["null" /* Reserve index 0 for an undefined function*/];
+
+// proxiedFunctionInvokers contains bodies of the functions that will perform the proxying. These
+// are generated in a map to keep track which ones have already been emitted, to avoid outputting duplicates.
+// map: pair(sig, syncOrAsync) -> function body
+var proxiedFunctionInvokers = {};
+
// JSifier
function JSify(data, functionsOnly) {
//B.start('jsifier');
@@ -100,6 +109,68 @@ function JSify(data, functionsOnly) {
return snippet;
}
+ // Generates a function that invokes a proxied function call from the calling thread to the main browser thread.
+ function generateProxiedCallInvoker(sig, sync /*async if false*/) {
+ if (sig.length == 0) throw 'Function signature cannot be empty!';
+ function argsList(num) { // ", p0, p1, p2, p3, p4"
+ var s = '';
+ for(var i = 0; i < num; ++i) s += ', p' + i;
+ return s;
+ }
+
+ var func = "function _emscripten_" + (sync ? '' : 'a') + 'sync_run_in_browser_thread_' + sig + '(func' + argsList(sig.length-1) + ') {\n';
+ if (sync) func += ' var waitAddress = Runtime.stackSave();\n';
+
+ function sizeofType(t) {
+ switch(t) {
+ case 'd':
+// case 'I': // int64 // TODO: For wasm, we'll have to have something like this
+// return 8;
+ case 'i':
+ case 'f':
+ return 4;
+ case 'v':
+ return 0;
+ // TODO: Smaller sizes?
+ default:
+ throw 'unsupported type in signature: ' + t;
+ }
+ }
+
+ var sizeofReturn = sizeofType(sig[0]);
+ if (sync) {
+ if (sizeofReturn == 4) func += ' var returnValue = waitAddress + 4;\n';
+ else if (sizeofReturn == 8) func += ' var returnValue = waitAddress + 8;\n'; // Retain alignment of each type
+ }
+
+ if (sync) func += ' Atomics.store(HEAP32, waitAddress >> 2, 0);\n';
+
+ function argsDict(num) { // ", p0: p0, p1: p1, p2: p2, p3: p3, p4: p4"
+ var s = '';
+ for(var i = 0; i < num; ++i) s += ', p' + i + ': p' + i;
+ return s;
+ }
+
+ // This is ad-hoc numbering scheme to map signatures to IDs, and must agree with call handler in src/library_pthread.js.
+ // Once proxied function calls no longer go through postMessage()s but instead in the heap, this will need to change, since int vs float will matter.
+ var functionCallOrdinal = sig.length + (sizeofReturn == 8 ? 20 : 0);
+
+ // next line generates a form: "postMessage({ proxiedCall: 9, func: func, waitAddress: waitAddress, returnValue: returnValue, p0: p0, p1: p1, p2: p2, p3: p3, p4: p4, p5: p5, p6: p6, p7: p7 });"
+ func += ' postMessage({ proxiedCall: ' + functionCallOrdinal + ', func: func' + (sync ? ', waitAddress: waitAddress' : '') + (sync && sizeofReturn > 0 ? ', returnValue: returnValue' : '') + argsDict(sig.length-1) + ' });\n';
+ if (sync) {
+ func += ' Atomics.wait(HEAP32, waitAddress >> 2, 0);\n';
+ switch(sig[0]) {
+ case 'i': func += ' return HEAP32[returnValue >> 2];\n'; break;
+ case 'f': func += ' return HEAPF32[returnValue >> 2];\n'; break;
+ case 'd': func += ' return HEAPF64[returnValue >> 3];\n'; break;
+ //case 'I': func += ' return HEAP64[returnValue >> 3];\n'; // TODO: For wasm
+ // TODO: Smaller sizes?
+ }
+ }
+ func += '}';
+ return func;
+ }
+
// functionStub
function functionStubHandler(item) {
// special logic
@@ -122,6 +193,11 @@ function JSify(data, functionsOnly) {
// dependencies can be JS functions, which we just run
if (typeof ident == 'function') return ident();
+ // don't process any special identifiers. These are looked up when processing the base name of the identifier.
+ if (ident.endsWith('__sig') || ident.endsWith('__proxy') || ident.endsWith('__asm') || ident.endsWith('__inline') || ident.endsWith('__deps') || ident.endsWith('__postset')) {
+ return '';
+ }
+
// $ident's are special, we do not prefix them with a '_'.
if (ident[0] === '$') {
var finalName = ident.substr(1);
@@ -225,7 +301,33 @@ function JSify(data, functionsOnly) {
var depsText = (deps ? '\n' + deps.map(addFromLibrary).filter(function(x) { return x != '' }).join('\n') : '');
var contentText;
if (isFunction) {
- contentText = snippet;
+ // Emit the body of a JS library function.
+ var proxyingMode = LibraryManager.library[ident + '__proxy'];
+ if (proxyingMode && proxyingMode !== 'main' && proxyingMode !== 'main_gl' && proxyingMode !== 'async' && proxyingMode !== 'async_gl') {
+ throw 'Invalid proxyingMode ' + ident + '__proxy: \'' + proxyingMode + '\' specified!';
+ }
+
+ if (USE_PTHREADS && proxyingMode) {
+ var sig = LibraryManager.library[ident + '__sig'];
+ if (!sig) throw 'Missing function signature field "' + ident + '__sig"! (Using proxying mode requires specifying the signature of the function)';
+ sig = sig.replace(/f/g, 'i'); // TODO: Implement float signatures.
+ var synchronousCall = (proxyingMode === 'main' || proxyingMode === 'main_gl');
+ var invokerKey = sig + (synchronousCall ? '_sync' : '_async');
+ if (!proxiedFunctionInvokers[invokerKey]) proxiedFunctionInvokers[invokerKey] = generateProxiedCallInvoker(sig, synchronousCall);
+ var proxyingCondition = (proxyingMode === 'main_gl' || proxyingMode === 'async_gl') ? 'GLctxIsOnParentThread' : 'ENVIRONMENT_IS_PTHREAD';
+ var proxyingFunc = synchronousCall ? '_emscripten_sync_run_in_browser_thread_' : '_emscripten_async_run_in_browser_thread_';
+ if (sig.length > 1) {
+ // If the function takes parameters, forward those to the proxied function call
+ snippet = snippet.replace(/function\s+(.*)?\s*\((.*?)\)\s*{/, 'function $1($2) {\nif (' + proxyingCondition + ') return ' + proxyingFunc + sig + '(' + proxiedFunctionTable.length + ', $2);');
+ } else {
+ // No parameters to the function
+ snippet = snippet.replace(/function (.*)? {/, 'function $1 {\nif (' + proxyingCondition + ') return ' + proxyingFunc + sig + '(' + proxiedFunctionTable.length + ');');
+ }
+ contentText = snippet;
+ proxiedFunctionTable.push(finalName);
+ } else {
+ contentText = snippet; // Regular JS function that will be executed in the context of the calling thread.
+ }
} else if (typeof snippet === 'string' && snippet.indexOf(';') == 0) {
contentText = 'var ' + finalName + snippet;
if (snippet[snippet.length-1] != ';' && snippet[snippet.length-1] != '}') contentText += ';';
@@ -402,7 +504,12 @@ function JSify(data, functionsOnly) {
legalizedI64s = legalizedI64sDefault;
if (!BUILD_AS_SHARED_LIB && !SIDE_MODULE) {
- if (USE_PTHREADS) print('if (!ENVIRONMENT_IS_PTHREAD) {\n // Only main thread initializes these, pthreads copy them over at thread worker init time (in pthread-main.js)');
+ if (USE_PTHREADS) {
+ print('\n // proxiedFunctionTable specifies the list of functions that can be called either synchronously or asynchronously from other threads in postMessage()d or internally queued events. This way a pthread in a Worker can synchronously access e.g. the DOM on the main thread.')
+ print('\nvar proxiedFunctionTable = [' + proxiedFunctionTable.join() + '];\n');
+ for(i in proxiedFunctionInvokers) print(proxiedFunctionInvokers[i]+'\n');
+ print('if (!ENVIRONMENT_IS_PTHREAD) {\n // Only main thread initializes these, pthreads copy them over at thread worker init time (in pthread-main.js)');
+ }
print('DYNAMICTOP_PTR = allocate(1, "i32", ALLOC_STATIC);\n');
print('STACK_BASE = STACKTOP = Runtime.alignMemory(STATICTOP);\n');
if (STACK_START > 0) print('if (STACKTOP < ' + STACK_START + ') STACK_BASE = STACKTOP = Runtime.alignMemory(' + STACK_START + ');\n');
--
2.14.1
From 5697581451fecb98b1ce965da77187a80b0c7b04 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= <jujjyl@gmail.com>
Date: Sat, 26 Aug 2017 20:42:00 +0300
Subject: [PATCH 03/23] Add test for synchronously proxying a call to main
thread and getting a value back.
---
src/library_pthread.js | 36 +++++++++++++++++++++++++++++++
tests/pthread/call_sync_on_main_thread.c | 30 ++++++++++++++++++++++++++
tests/pthread/call_sync_on_main_thread.js | 28 ++++++++++++++++++++++++
tests/test_browser.py | 8 ++++++-
4 files changed, 101 insertions(+), 1 deletion(-)
create mode 100644 tests/pthread/call_sync_on_main_thread.c
create mode 100644 tests/pthread/call_sync_on_main_thread.js
diff --git a/src/library_pthread.js b/src/library_pthread.js
index f438799b7..309717623 100644
--- a/src/library_pthread.js
+++ b/src/library_pthread.js
@@ -262,6 +262,42 @@ var LibraryPThread = {
var worker = new Worker(pthreadMainJs);
worker.onmessage = function(e) {
+ // TODO: Move the proxied call mechanism into a queue inside heap.
+ if (e.data.proxiedCall) {
+ var returnValue;
+ var funcTable = (e.data.func >= 0) ? proxiedFunctionTable : ASM_CONSTS;
+ var funcIdx = (e.data.func >= 0) ? e.data.func : (-1 - e.data.func);
+ PThread.currentProxiedOperationCallerThread = worker.pthread.threadInfoStruct; // Sometimes we need to backproxy events to the calling thread (e.g. HTML5 DOM events handlers such as emscripten_set_mousemove_callback()), so keep track in a globally accessible variable about the thread that initiated the proxying.
+ switch(e.data.proxiedCall) {
+ case 1: case 21: returnValue = funcTable[funcIdx](); break;
+ case 2: returnValue = funcTable[funcIdx](e.data.p0); break;
+ case 3: returnValue = funcTable[funcIdx](e.data.p0, e.data.p1); break;
+ case 4: returnValue = funcTable[funcIdx](e.data.p0, e.data.p1, e.data.p2); break;
+ case 5: returnValue = funcTable[funcIdx](e.data.p0, e.data.p1, e.data.p2, e.data.p3); break;
+ case 6: returnValue = funcTable[funcIdx](e.data.p0, e.data.p1, e.data.p2, e.data.p3, e.data.p4); break;
+ case 7: returnValue = funcTable[funcIdx](e.data.p0, e.data.p1, e.data.p2, e.data.p3, e.data.p4, e.data.p5); break;
+ case 8: returnValue = funcTable[funcIdx](e.data.p0, e.data.p1, e.data.p2, e.data.p3, e.data.p4, e.data.p5, e.data.p6); break;
+ case 9: returnValue = funcTable[funcIdx](e.data.p0, e.data.p1, e.data.p2, e.data.p3, e.data.p4, e.data.p5, e.data.p6, e.data.p7); break;
+ case 10: returnValue = funcTable[funcIdx](e.data.p0, e.data.p1, e.data.p2, e.data.p3, e.data.p4, e.data.p5, e.data.p6, e.data.p7, e.data.p8); break;
+ default:
+ if (e.data.proxiedCall) {
+ Module['printErr']("worker sent an unknown proxied call idx " + e.data.proxiedCall);
+ console.error(e.data);
+ }
+ break;
+ }
+ if (e.data.returnValue) {
+ if (e.data.proxiedCall != 21) HEAP32[e.data.returnValue >> 2] = returnValue;
+ else HEAPF64[e.data.returnValue >> 3] = returnValue;
+ }
+ var waitAddress = e.data.waitAddress;
+ if (waitAddress) {
+ Atomics.store(HEAP32, waitAddress >> 2, 1);
+ Atomics.wake(HEAP32, waitAddress >> 2, 1);
+ }
+ return;
+ }
+
// If this message is intended to a recipient that is not the main thread, forward it to the target thread.
if (e.data.targetThread && e.data.targetThread != _pthread_self()) {
var thread = PThread.pthreads[e.data.targetThread];
diff --git a/tests/pthread/call_sync_on_main_thread.c b/tests/pthread/call_sync_on_main_thread.c
new file mode 100644
index 000000000..2f4ca38aa
--- /dev/null
+++ b/tests/pthread/call_sync_on_main_thread.c
@@ -0,0 +1,30 @@
+#include <stdio.h>
+#include <assert.h>
+#include <string.h>
+
+extern void getDomElementInnerHTML(const char *domElement, char *dst, int size);
+extern int isThisInWorker(void);
+extern int isThisInWorkerOnMainThread(void);
+extern int receivesAndReturnsAnInteger(int i);
+
+// Define this if compiling via -s PROXY_TO_PTHREAD=1
+// #define PROXY_TO_PTHREAD 1
+
+int main()
+{
+ char dst[256];
+ char name[7] = "resize";
+ getDomElementInnerHTML(name, dst, sizeof(dst));
+ memset(name, 0, sizeof(name)); // Try to uncover if there might be a race condition and above line was not synchronously processed, and we could take name string away.
+ int inWorker1 = isThisInWorker(); // Build this application with -s USE_PTHREADS=1 -s PROXY_TO_PTHREAD=1 for this to return 1, otherwise returns 0.
+ int inWorker2 = isThisInWorkerOnMainThread(); // This should always return 0
+ int returnedInt = receivesAndReturnsAnInteger(4);
+ printf("text: \"%s\". inWorker1: %d, inWorker2: %d, returnedInt: %d\n", dst, inWorker1, inWorker2, returnedInt);
+ assert(!strstr(dst, "Resize canvas"));
+ assert(inWorker1 == PROXY_TO_PTHREAD);
+ assert(inWorker2 == 0);
+ assert(returnedInt == 42 + 4);
+#ifdef REPORT_RESULT
+ REPORT_RESULT(1);
+#endif
+}
diff --git a/tests/pthread/call_sync_on_main_thread.js b/tests/pthread/call_sync_on_main_thread.js
new file mode 100644
index 000000000..3a9e57b44
--- /dev/null
+++ b/tests/pthread/call_sync_on_main_thread.js
@@ -0,0 +1,28 @@
+mergeInto(LibraryManager.library, {
+ // Test accessing a DOM element on the main thread.
+ // This function returns the inner text of the div by ID "status"
+ // Because it accesses the DOM, it must be called on the main thread.
+ getDomElementInnerHTML__proxy: 'main',
+ getDomElementInnerHTML__sig: 'viii',
+ getDomElementInnerHTML: function(domElementId, dst, size) {
+ var id = UTF8ToString(domElementId);
+ var text = document.getElementById(id).innerHTML;
+ stringToUTF8(text, dst, size);
+ },
+
+ receivesAndReturnsAnInteger__proxy: 'main',
+ receivesAndReturnsAnInteger__sig: 'ii',
+ receivesAndReturnsAnInteger: function(i) {
+ return i + 42;
+ },
+
+ isThisInWorker: function() {
+ return ENVIRONMENT_IS_WORKER;
+ },
+
+ isThisInWorkerOnMainThread__proxy: 'main',
+ isThisInWorkerOnMainThread__sig: 'i',
+ isThisInWorkerOnMainThread: function() {
+ return ENVIRONMENT_IS_WORKER;
+ }
+});
diff --git a/tests/test_browser.py b/tests/test_browser.py
index 033248405..0f21e7e64 100644
--- a/tests/test_browser.py
+++ b/tests/test_browser.py
@@ -3318,6 +3318,12 @@ window.close = function() {
def test_pthread_run_on_main_thread_flood(self):
self.btest(path_from_root('tests', 'pthread', 'test_pthread_run_on_main_thread_flood.cpp'), expected='0', args=['-O3', '-s', 'USE_PTHREADS=2', '-s', 'PTHREAD_POOL_SIZE=1', '--separate-asm'], timeout=30)
+ # Test that it is possible to synchronously call a JavaScript function on the main thread and get a return value back.
+ def test_pthread_call_sync_on_main_thread(self):
+ self.btest(path_from_root('tests', 'pthread', 'call_sync_on_main_thread.c'), expected='1', args=['-O3', '-s', 'USE_PTHREADS=1', '-s', 'PROXY_TO_PTHREAD=1', '-DPROXY_TO_PTHREAD=1', '--js-library', path_from_root('tests', 'pthread', 'call_sync_on_main_thread.js')])
+ self.btest(path_from_root('tests', 'pthread', 'call_sync_on_main_thread.c'), expected='1', args=['-O3', '-s', 'USE_PTHREADS=1', '-DPROXY_TO_PTHREAD=0', '--js-library', path_from_root('tests', 'pthread', 'call_sync_on_main_thread.js')])
+ self.btest(path_from_root('tests', 'pthread', 'call_sync_on_main_thread.c'), expected='1', args=['-Oz', '-DPROXY_TO_PTHREAD=0', '--js-library', path_from_root('tests', 'pthread', 'call_sync_on_main_thread.js')])
+
# test atomicrmw i64
def test_atomicrmw_i64(self):
Popen([PYTHON, EMCC, path_from_root('tests', 'atomicrmw_i64.ll'), '-s', 'USE_PTHREADS=1', '-s', 'IN_TEST_HARNESS=1', '-o', 'test.html']).communicate()
@@ -3623,4 +3629,4 @@ window.close = function() {
# Tests the Emscripten HTML5 API emscripten_set_canvas_element_size() and emscripten_get_canvas_element_size() functionality in singlethreaded programs.
def test_emscripten_set_canvas_element_size(self):
- self.btest('emscripten_set_canvas_element_size.c', expected='1')
\ No newline at end of file
+ self.btest('emscripten_set_canvas_element_size.c', expected='1')
--
2.14.1
From c90c09ce45d2dc4b1b0a6a714372a2d4731cd2f0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= <jujjyl@gmail.com>
Date: Sat, 26 Aug 2017 21:15:56 +0300
Subject: [PATCH 04/23] Add a test for asynchronously proxying calls from a
pthread to the main thread.
---
tests/pthread/call_async_on_main_thread.c | 10 ++++++++++
tests/pthread/call_async_on_main_thread.js | 12 ++++++++++++
tests/runner.py | 2 +-
tests/test_browser.py | 6 ++++++
4 files changed, 29 insertions(+), 1 deletion(-)
create mode 100644 tests/pthread/call_async_on_main_thread.c
create mode 100644 tests/pthread/call_async_on_main_thread.js
diff --git a/tests/pthread/call_async_on_main_thread.c b/tests/pthread/call_async_on_main_thread.c
new file mode 100644
index 000000000..b46f5f83c
--- /dev/null
+++ b/tests/pthread/call_async_on_main_thread.c
@@ -0,0 +1,10 @@
+#include <stdio.h>
+#include <assert.h>
+#include <string.h>
+
+extern void report_result(int param1, int param2, int param3);
+
+int main()
+{
+ report_result(1, 2, 3);
+}
diff --git a/tests/pthread/call_async_on_main_thread.js b/tests/pthread/call_async_on_main_thread.js
new file mode 100644
index 000000000..f70198dc5
--- /dev/null
+++ b/tests/pthread/call_async_on_main_thread.js
@@ -0,0 +1,12 @@
+mergeInto(LibraryManager.library, {
+ // Test asynchronously calling a function on the main thread.
+ report_result__proxy: 'async',
+ report_result__sig: 'viii',
+ report_result: function(param1, param2, param3) {
+ if (ENVIRONMENT_IS_WORKER) {
+ console.error('This function should be getting called on the main thread!');
+ }
+ console.log('got ' + param1 + ' ' + param2 + ' ' + param3);
+ __ReportResult(param1 + param2 * param3, 0);
+ }
+});
diff --git a/tests/runner.py b/tests/runner.py
index 3054e5de2..05da71883 100755
--- a/tests/runner.py
+++ b/tests/runner.py
@@ -819,7 +819,7 @@ class BrowserCore(RunnerCore):
#define __REPORT_RESULT_DEFINED__
#include <emscripten.h>
- static void _ReportResult(int result, int sync)
+ static void EMSCRIPTEN_KEEPALIVE _ReportResult(int result, int sync)
{
EM_ASM({
var xhr = new XMLHttpRequest();
diff --git a/tests/test_browser.py b/tests/test_browser.py
index 0f21e7e64..b487e5480 100644
--- a/tests/test_browser.py
+++ b/tests/test_browser.py
@@ -3324,6 +3324,12 @@ window.close = function() {
self.btest(path_from_root('tests', 'pthread', 'call_sync_on_main_thread.c'), expected='1', args=['-O3', '-s', 'USE_PTHREADS=1', '-DPROXY_TO_PTHREAD=0', '--js-library', path_from_root('tests', 'pthread', 'call_sync_on_main_thread.js')])
self.btest(path_from_root('tests', 'pthread', 'call_sync_on_main_thread.c'), expected='1', args=['-Oz', '-DPROXY_TO_PTHREAD=0', '--js-library', path_from_root('tests', 'pthread', 'call_sync_on_main_thread.js')])
+ # Test that it is possible to asynchronously call a JavaScript function on the main thread.
+ def test_pthread_call_async_on_main_thread(self):
+ self.btest(path_from_root('tests', 'pthread', 'call_async_on_main_thread.c'), expected='7', args=['-O3', '-s', 'USE_PTHREADS=1', '-s', 'PROXY_TO_PTHREAD=1', '-DPROXY_TO_PTHREAD=1', '--js-library', path_from_root('tests', 'pthread', 'call_async_on_main_thread.js')])
+ self.btest(path_from_root('tests', 'pthread', 'call_async_on_main_thread.c'), expected='7', args=['-O3', '-s', 'USE_PTHREADS=1', '-DPROXY_TO_PTHREAD=0', '--js-library', path_from_root('tests', 'pthread', 'call_async_on_main_thread.js')])
+ self.btest(path_from_root('tests', 'pthread', 'call_async_on_main_thread.c'), expected='7', args=['-Oz', '-DPROXY_TO_PTHREAD=0', '--js-library', path_from_root('tests', 'pthread', 'call_async_on_main_thread.js')])
+
# test atomicrmw i64
def test_atomicrmw_i64(self):
Popen([PYTHON, EMCC, path_from_root('tests', 'atomicrmw_i64.ll'), '-s', 'USE_PTHREADS=1', '-s', 'IN_TEST_HARNESS=1', '-o', 'test.html']).communicate()
--
2.14.1
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment