Initial iree_loop_t implementation for web browsers. (#11204)
This is the start of an implementation of the
[`iree_loop_t`](https://github.com/iree-org/iree/blob/main/runtime/src/iree/base/loop.h)
interface (added back in https://github.com/iree-org/iree/pull/8329) for
web browsers, using `setTimeout()`, Promises, and other JavaScript APIs
for working with asynchronous operations and the browser event loop.
This only handles `IREE_LOOP_COMMAND_CALL` for now, but I'd like some
feedback on the architecture / implementation style.
Some details that were tricky to figure out:
* This uses a JavaScript library that implements a C API:
https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html#implement-a-c-api-in-javascript.
We may end up with a few of these.
* The JavaScript library uses `dynCall_iiii` to call a provided C
function pointer (I haven't found any dedicated documentation on this,
so this was pieced together from reading through Emscripten's source
code)
* A new `iree_link_js_library` CMake function was added to pass
`--js-library` to `emcc` and set up the right dependency link between
.js files and binaries (tests/applications/etc.). We can also use
`--pre-js` and `--post-js` as needed.
diff --git a/build_tools/cmake/iree_macros.cmake b/build_tools/cmake/iree_macros.cmake
index 2b81f59..3e90931 100644
--- a/build_tools/cmake/iree_macros.cmake
+++ b/build_tools/cmake/iree_macros.cmake
@@ -267,6 +267,81 @@
endfunction()
#-------------------------------------------------------------------------------
+# Emscripten
+#-------------------------------------------------------------------------------
+
+# A global counter to guarantee unique names for js library files.
+set(_LINK_JS_COUNTER 1)
+
+# Links a JavaScript library to a target using --js-library=file.js.
+#
+# This function is only supported when running under Emscripten (emcmake).
+# This implementation is forked from `em_add_tracked_link_flag()` in
+# https://github.com/emscripten-core/emscripten/blob/main/cmake/Modules/Platform/Emscripten.cmake
+# with changes to be compatible with IREE project style and CMake conventions.
+#
+# Parameters:
+# TARGET: Name of the target to link against
+# SRCS: List of JavaScript source files to link
+function(iree_link_js_library)
+ cmake_parse_arguments(
+ _RULE
+ ""
+ "TARGET"
+ "SRCS"
+ ${ARGN}
+ )
+
+ # Convert from aliased, possibly package-relative, names to target names.
+ iree_package_ns(_PACKAGE_NS)
+ string(REGEX REPLACE "^::" "${_PACKAGE_NS}::" _RULE_TARGET ${_RULE_TARGET})
+ string(REPLACE "::" "_" _RULE_TARGET ${_RULE_TARGET})
+
+ foreach(_SRC_FILE ${_RULE_SRCS})
+ # If the JS file is changed, we want to relink dependent binaries, but
+ # unfortunately it is not possible to make a link step depend directly on a
+ # source file. Instead, we must make a dummy no-op build target on that
+ # source file, and make the original target depend on that dummy target.
+
+ # Sanitate the source .js filename to a good dummy filename.
+ get_filename_component(_JS_NAME "${_SRC_FILE}" NAME)
+ string(REGEX REPLACE "[/:\\\\.\ ]" "_" _DUMMY_JS_TARGET ${_JS_NAME})
+ set(_DUMMY_LIB_NAME ${_RULE_TARGET}_${_LINK_JS_COUNTER}_${_DUMMY_JS_TARGET})
+ set(_DUMMY_C_NAME "${CMAKE_BINARY_DIR}/${_DUMMY_JS_TARGET}_tracker.c")
+
+ # Create a new static library target that with a single dummy .c file.
+ add_library(${_DUMMY_LIB_NAME} STATIC ${_DUMMY_C_NAME})
+ # Make the dummy .c file depend on the .js file we are linking, so that if
+ # the .js file is edited, the dummy .c file, and hence the static library
+ # will be rebuild (no-op). This causes the main application to be
+ # relinked, which is what we want. This approach was recommended by
+ # http://www.cmake.org/pipermail/cmake/2010-May/037206.html
+ add_custom_command(
+ OUTPUT ${_DUMMY_C_NAME}
+ COMMAND ${CMAKE_COMMAND} -E touch ${_DUMMY_C_NAME}
+ DEPENDS ${_SRC_FILE}
+ )
+ target_link_libraries(${_RULE_TARGET}
+ PUBLIC
+ ${_DUMMY_LIB_NAME}
+ )
+
+ # Link the js-library to the target.
+ # When a linked library starts with a "-" cmake will just add it to the
+ # linker command line as it is. The advantage of doing it this way is
+ # that the js-library will also be automatically linked to targets that
+ # depend on this target.
+ get_filename_component(_SRC_ABSOLUTE_PATH "${_SRC_FILE}" ABSOLUTE)
+ target_link_libraries(${_RULE_TARGET}
+ PUBLIC
+ "--js-library \"${_SRC_ABSOLUTE_PATH}\""
+ )
+
+ math(EXPR _LINK_JS_COUNTER "${_LINK_JS_COUNTER} + 1")
+ endforeach()
+endfunction()
+
+#-------------------------------------------------------------------------------
# Tool symlinks
#-------------------------------------------------------------------------------
diff --git a/runtime/src/iree/base/CMakeLists.txt b/runtime/src/iree/base/CMakeLists.txt
index 2bdaf40..c962f95 100644
--- a/runtime/src/iree/base/CMakeLists.txt
+++ b/runtime/src/iree/base/CMakeLists.txt
@@ -189,3 +189,40 @@
PUBLIC
)
endif()
+
+if(EMSCRIPTEN)
+ iree_cc_library(
+ NAME
+ loop_emscripten
+ HDRS
+ "loop_emscripten.h"
+ SRCS
+ "loop_emscripten.c"
+ DEPS
+ ::base
+ PUBLIC
+ )
+
+ iree_link_js_library(
+ TARGET
+ ::loop_emscripten
+ SRCS
+ "loop_emscripten.js"
+ )
+
+ # TODO(scotttodd): rewrite as a JS/C file and test with Promises
+ # The C++ test uses IREE_LOOP_COMMAND_DRAIN, which is not implemented here
+ # This test is just useful for the initial bring-up.
+ iree_cc_test(
+ NAME
+ loop_emscripten_test
+ SRCS
+ "loop_emscripten_test.cc"
+ DEPS
+ ::base
+ ::loop_emscripten
+ ::loop_test_hdrs
+ iree::testing::gtest
+ iree::testing::gtest_main
+ )
+endif()
diff --git a/runtime/src/iree/base/loop_emscripten.c b/runtime/src/iree/base/loop_emscripten.c
new file mode 100644
index 0000000..8b4bd50
--- /dev/null
+++ b/runtime/src/iree/base/loop_emscripten.c
@@ -0,0 +1,93 @@
+// Copyright 2022 The IREE Authors
+//
+// Licensed under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#include "iree/base/loop_emscripten.h"
+
+#if defined(IREE_PLATFORM_EMSCRIPTEN)
+
+#include <emscripten.h>
+
+#include "iree/base/assert.h"
+
+//===----------------------------------------------------------------------===//
+// externs from loop_emscripten.js
+//===----------------------------------------------------------------------===//
+
+extern iree_status_t loop_command_call(iree_loop_callback_fn_t callback,
+ void* user_data, iree_loop_t loop);
+
+//===----------------------------------------------------------------------===//
+// iree_loop_emscripten_t
+//===----------------------------------------------------------------------===//
+
+typedef struct iree_loop_emscripten_t {
+ iree_allocator_t allocator;
+
+ // TODO(scotttodd): handle to a "scope"/object (managed in JS), so multiple
+ // loops can exist at once
+} iree_loop_emscripten_t;
+
+IREE_API_EXPORT iree_status_t iree_loop_emscripten_allocate(
+ iree_allocator_t allocator, iree_loop_emscripten_t** out_loop) {
+ IREE_ASSERT_ARGUMENT(out_loop);
+ iree_loop_emscripten_t* loop = NULL;
+ IREE_RETURN_IF_ERROR(
+ iree_allocator_malloc(allocator, sizeof(*loop), (void**)&loop));
+ loop->allocator = allocator;
+ *out_loop = loop;
+ return iree_ok_status();
+}
+
+IREE_API_EXPORT void iree_loop_emscripten_free(iree_loop_emscripten_t* loop) {
+ IREE_ASSERT_ARGUMENT(loop);
+ iree_allocator_t allocator = loop->allocator;
+
+ // TODO(scotttodd): cleanup:
+ // abort pending operations (neuter callbacks/Promises)
+ // assert if any work is still outstanding
+
+ // After all operations are cleared we can release the data structures.
+ iree_allocator_free(allocator, loop);
+}
+
+static iree_status_t iree_loop_emscripten_run_call(
+ iree_loop_emscripten_t* loop_emscripten, iree_loop_call_params_t* params) {
+ iree_loop_t loop = iree_loop_emscripten(loop_emscripten);
+ return loop_command_call(params->callback.fn, params->callback.user_data,
+ loop);
+}
+
+// Control function for the Emscripten loop.
+IREE_API_EXPORT iree_status_t
+iree_loop_emscripten_ctl(void* self, iree_loop_command_t command,
+ const void* params, void** inout_ptr) {
+ IREE_ASSERT_ARGUMENT(self);
+
+ iree_loop_emscripten_t* loop_emscripten = (iree_loop_emscripten_t*)self;
+
+ // NOTE: we return immediately to make this all (hopefully) tail calls.
+ switch (command) {
+ case IREE_LOOP_COMMAND_CALL:
+ return iree_loop_emscripten_run_call(loop_emscripten,
+ (iree_loop_call_params_t*)params);
+ case IREE_LOOP_COMMAND_DISPATCH:
+ case IREE_LOOP_COMMAND_WAIT_UNTIL:
+ case IREE_LOOP_COMMAND_WAIT_ONE:
+ case IREE_LOOP_COMMAND_WAIT_ALL:
+ case IREE_LOOP_COMMAND_WAIT_ANY:
+ // TODO(scotttodd): implement these commands
+ return iree_make_status(IREE_STATUS_UNIMPLEMENTED,
+ "unimplemented loop command");
+ case IREE_LOOP_COMMAND_DRAIN:
+ return iree_make_status(IREE_STATUS_DEADLINE_EXCEEDED,
+ "unsupported loop command");
+ default:
+ return iree_make_status(IREE_STATUS_UNIMPLEMENTED,
+ "unimplemented loop command");
+ }
+}
+
+#endif // IREE_PLATFORM_EMSCRIPTEN
diff --git a/runtime/src/iree/base/loop_emscripten.h b/runtime/src/iree/base/loop_emscripten.h
new file mode 100644
index 0000000..4f8c6b7
--- /dev/null
+++ b/runtime/src/iree/base/loop_emscripten.h
@@ -0,0 +1,53 @@
+// Copyright 2022 The IREE Authors
+//
+// Licensed under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#ifndef IREE_BASE_LOOP_EMSCRIPTEN_H_
+#define IREE_BASE_LOOP_EMSCRIPTEN_H_
+
+#include "iree/base/api.h"
+
+#if defined(IREE_PLATFORM_EMSCRIPTEN)
+
+#ifdef __cplusplus
+extern "C" {
+#endif // __cplusplus
+
+//===----------------------------------------------------------------------===//
+// iree_loop_emscripten_t
+//===----------------------------------------------------------------------===//
+
+// A loop backed by the web browser event loop, built using Emscripten.
+// TODO(scotttodd): comment on thread safety (when established)
+typedef struct iree_loop_emscripten_t iree_loop_emscripten_t;
+
+// Allocates a loop using |allocator| stored into |out_loop|.
+IREE_API_EXPORT iree_status_t iree_loop_emscripten_allocate(
+ iree_allocator_t allocator, iree_loop_emscripten_t** out_loop);
+
+// Frees |loop_emscripten|, aborting all pending operations.
+IREE_API_EXPORT void iree_loop_emscripten_free(iree_loop_emscripten_t* loop);
+
+IREE_API_EXPORT iree_status_t
+iree_loop_emscripten_ctl(void* self, iree_loop_command_t command,
+ const void* params, void** inout_ptr);
+
+// Returns a loop that uses |data|.
+// TODO(scotttodd): rework structs with "scope" so 2+ loops can exist at once
+static inline iree_loop_t iree_loop_emscripten(iree_loop_emscripten_t* data) {
+ iree_loop_t loop = {
+ data,
+ iree_loop_emscripten_ctl,
+ };
+ return loop;
+}
+
+#ifdef __cplusplus
+} // extern "C"
+#endif // __cplusplus
+
+#endif // IREE_PLATFORM_EMSCRIPTEN
+
+#endif // IREE_BASE_LOOP_EMSCRIPTEN_H_
diff --git a/runtime/src/iree/base/loop_emscripten.js b/runtime/src/iree/base/loop_emscripten.js
new file mode 100644
index 0000000..06190d3
--- /dev/null
+++ b/runtime/src/iree/base/loop_emscripten.js
@@ -0,0 +1,43 @@
+// Copyright 2022 The IREE Authors
+//
+// Licensed under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+// This is the JavaScript side of loop_emscripten.c
+//
+// References:
+// * https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html
+// * https://github.com/evanw/emscripten-library-generator
+// * https://github.com/emscripten-core/emscripten/tree/main/src
+
+const LibraryLoopEmscripten = {
+ $loop_emscripten_support__postset: 'loop_emscripten_support();',
+ $loop_emscripten_support: function() {
+ class LoopEmscripten {
+ constructor() {
+ // TODO(scotttodd): store state here
+ }
+
+ loop_command_call(callback, user_data, loop) {
+ const IREE_STATUS_OK = 0;
+
+ setTimeout(() => {
+ const ret =
+ Module['dynCall_iiii'](callback, user_data, loop, IREE_STATUS_OK);
+ // TODO(scotttodd): handle the returned status (sticky failure state?)
+ }, 0);
+
+ return IREE_STATUS_OK;
+ }
+ }
+
+ const instance = new LoopEmscripten();
+ _loop_command_call = instance.loop_command_call.bind(instance);
+ },
+
+ loop_command_call: function() {},
+ loop_command_call__deps: ['$loop_emscripten_support'],
+}
+
+mergeInto(LibraryManager.library, LibraryLoopEmscripten);
diff --git a/runtime/src/iree/base/loop_emscripten_test.cc b/runtime/src/iree/base/loop_emscripten_test.cc
new file mode 100644
index 0000000..3fe7f02
--- /dev/null
+++ b/runtime/src/iree/base/loop_emscripten_test.cc
@@ -0,0 +1,31 @@
+// Copyright 2022 The IREE Authors
+//
+// Licensed under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+// TODO(scotttodd): rewrite as a JS/C file and test with Promises
+// The C++ test uses IREE_LOOP_COMMAND_DRAIN, which is not implemented here
+
+#include "iree/base/loop_emscripten.h"
+
+#include "iree/base/api.h"
+#include "iree/testing/gtest.h"
+#include "iree/testing/status_matchers.h"
+
+// Contains the test definitions applied to all loop implementations:
+#include "iree/base/loop_test.h"
+
+void AllocateLoop(iree_status_t* out_status, iree_allocator_t allocator,
+ iree_loop_t* out_loop) {
+ iree_loop_emscripten_t* loop_emscripten = NULL;
+ IREE_CHECK_OK(iree_loop_emscripten_allocate(allocator, &loop_emscripten));
+
+ *out_status = iree_ok_status();
+ *out_loop = iree_loop_emscripten(loop_emscripten);
+}
+
+void FreeLoop(iree_allocator_t allocator, iree_loop_t loop) {
+ iree_loop_emscripten_t* loop_emscripten = (iree_loop_emscripten_t*)loop.self;
+ iree_loop_emscripten_free(loop_emscripten);
+}