Add dynamic linking web sample using Emscripten. (#8319)

Like the static web sample introduced in https://github.com/google/iree/pull/8171, this uses Emscripten to compile IREE's runtime to WebAssembly. What's new here is the use of Emscripten's [runtime dynamic linking support](https://emscripten.org/docs/compiling/Dynamic-Linking.html#runtime-dynamic-linking-with-dlopen) to be able to separately serve and load compiled programs.

The sample currenly loads [`simple_abs.mlir`](https://github.com/google/iree/blob/main/iree/samples/models/simple_abs.mlir), queries some metadata, then calls the exported function:

https://github.com/google/iree/blob/b959830d762b0614d42a0cf3d2a623410cb3247a/iree/samples/models/simple_abs.mlir#L1-L4

```
iree_worker.js:20 WebAssembly module onRuntimeInitialized()
(index):34 IREE initialized, loading program...
iree_worker.js:75 worker received message: {messageType: 'loadProgram', id: 0, payload: './simple_abs.vmfb'}
iree_worker.js:33 fetching program at './simple_abs.vmfb'
iree_worker.js:38 XMLHttpRequest completed, passing to Wasm module
iree_worker.js:14 (C) load_program() received 5158 bytes of data
iree_worker.js:14 (C) === module properties ===
iree_worker.js:14 (C)   module name: 'module'
iree_worker.js:14 (C)   module signature:
iree_worker.js:14 (C)     18 imported functions
iree_worker.js:14 (C)     2 exported functions
iree_worker.js:14 (C)     2 internal functions
iree_worker.js:14 (C)   exported functions:
iree_worker.js:14 (C)     function name: 'abs', calling convention: 0r_r'
iree_worker.js:14 (C)     function name: '__init', calling convention: 0v_v'
iree_worker.js:14 (C) abs(-5.500000) -> result: 5.500000
iree_worker.js:49 Result from loadProgramFn(): 0
(index):37 Load program success!
```

Future work may add drag-and-drop -> `iree-run-module` behavior. I also have some ideas for building a little UI that shows you properties of any .vmfb like which executable formats are included in it.

I explored enabling multithreading together with the dynamic linking, but ran into issues with Emscripten's experimental [dynamic linking + pthreads](https://emscripten.org/docs/compiling/Dynamic-Linking.html#pthreads-support)  implementation. I left some notes on this at the bottom of the new `README.md`.
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 695c3d3..bcfb6b9 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -602,7 +602,7 @@
 endif()
 
 if(${IREE_BUILD_EXPERIMENTAL_WEB_SAMPLES})
-  add_subdirectory(experimental/sample_web_static)
+  add_subdirectory(experimental/web)
 endif()
 
 set(IREE_PUBLIC_INCLUDE_DIRS "${IREE_COMMON_INCLUDE_DIRS}"
diff --git a/experimental/web/CMakeLists.txt b/experimental/web/CMakeLists.txt
new file mode 100644
index 0000000..3a57a21
--- /dev/null
+++ b/experimental/web/CMakeLists.txt
@@ -0,0 +1,11 @@
+# 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
+
+if(NOT EMSCRIPTEN)
+  return()
+endif()
+
+iree_add_all_subdirs()
diff --git a/experimental/web/sample_dynamic/CMakeLists.txt b/experimental/web/sample_dynamic/CMakeLists.txt
new file mode 100644
index 0000000..f1391f5
--- /dev/null
+++ b/experimental/web/sample_dynamic/CMakeLists.txt
@@ -0,0 +1,48 @@
+# 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
+
+if(NOT EMSCRIPTEN)
+  return()
+endif()
+
+#-------------------------------------------------------------------------------
+# Sync
+#-------------------------------------------------------------------------------
+
+set(_NAME "iree_experimental_web_sample_dynamic_sync")
+add_executable(${_NAME} "")
+target_sources(${_NAME}
+  PRIVATE
+    main.c
+    device_sync.c
+)
+set_target_properties(${_NAME} PROPERTIES OUTPUT_NAME "web-sample-dynamic-sync")
+
+# Note: we have to be very careful about dependencies here.
+#
+# The general purpose libraries link in multiple executable loaders and HAL
+# drivers/devices, which include code not compatible with Emscripten.
+target_link_libraries(${_NAME}
+  iree_runtime_runtime
+  iree_hal_local_loaders_system_library_loader
+  iree_hal_local_sync_driver
+)
+
+target_link_options(${_NAME} PRIVATE
+  # https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html#interacting-with-code-ccall-cwrap
+  "-sEXPORTED_FUNCTIONS=['_load_program']"
+  "-sEXPORTED_RUNTIME_METHODS=['ccall','cwrap']"
+  #
+  "-sASSERTIONS=1"
+  #
+  # https://developer.chrome.com/blog/wasm-debugging-2020/
+  "-g"
+  "-gseparate-dwarf"
+  #
+  # Dynamic linking: https://emscripten.org/docs/compiling/Dynamic-Linking.html
+  "-sMAIN_MODULE"
+  # "-sALLOW_TABLE_GROWTH"
+)
diff --git a/experimental/web/sample_dynamic/README.md b/experimental/web/sample_dynamic/README.md
new file mode 100644
index 0000000..2762e78
--- /dev/null
+++ b/experimental/web/sample_dynamic/README.md
@@ -0,0 +1,52 @@
+# Dynamic Web Sample
+
+This experimental sample demonstrates one way to target the web platform with
+IREE. The output artifact is a web page that loads a separately provided IREE
+`.vmfb` (compiled ML model) and tests calling functions on it.
+
+## Quickstart
+
+1. Install IREE's host tools (e.g. by building the `install` target with CMake)
+2. Install the Emscripten SDK by
+   [following these directions](https://emscripten.org/docs/getting_started/downloads.html)
+3. Initialize your Emscripten environment (e.g. run `emsdk_env.bat`)
+4. From this directory, run `bash ./build_sample.sh`
+    * You may need to set the path to your host tools install
+5. Open the localhost address linked in the script output
+
+To rebuild most parts of the sample (C runtime, sample HTML, CMake config,
+etc.), just `control + C` to stop the local webserver and rerun the script.
+
+## How it works
+
+[Emscripten](https://emscripten.org/) is used (via the `emcmake` CMake wrapper)
+to compile the runtime into WebAssembly and JavaScript files.
+
+Any supported IREE program, such as
+[simple_abs.mlir](../../../iree/samples/models/simple_abs.mlir), is compiled using
+the "system library" linking mode (i.e. `--iree-llvm-link-embedded=false`).
+This creates a shared object (typically .so/.dll, .wasm in this case). When the
+runtime attempts to load this file using `dlopen()` and `dlsym()`, Emscripten
+makes use of its
+[runtime dynamic linking support](https://emscripten.org/docs/compiling/Dynamic-Linking.html#runtime-dynamic-linking-with-dlopen)
+to instantiate a new `WebAssembly.Instance` which shares memory with the main
+runtime then resolve each export provided by the new Wasm module.
+
+### Asynchronous API
+
+* [`iree_api.js`](./iree_api.js) exposes a Promise-based API to the hosting
+  application in [`index.html`](./index.html)
+* [`iree_api.js`](./iree_api.js) creates a worker running iree_worker.js, which
+  includes Emscripten's JS code and instantiates the WebAssembly module
+* messages are passed back and forth between [`iree_api.js`](./iree_api.js) and
+  [`iree_worker.js`](./iree_worker.js) internally
+
+### Multithreading
+
+Multithreading is _not supported yet_. Emscripten only has experimental support
+for dynamic linking + pthreads:
+https://emscripten.org/docs/compiling/Dynamic-Linking.html#pthreads-support.
+Compiled programs produced by IREE link with `wasm-ld`, while Emscripten expects
+programs to be linked using `emcc` with the `-s SIDE_MODULE` option, which
+includes several Emscripten-pthreads-specific module exported functions such as
+`emscripten_tls_init`.
diff --git a/experimental/web/sample_dynamic/build_sample.sh b/experimental/web/sample_dynamic/build_sample.sh
new file mode 100644
index 0000000..afce01e
--- /dev/null
+++ b/experimental/web/sample_dynamic/build_sample.sh
@@ -0,0 +1,86 @@
+#!/bin/bash
+# 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
+
+set -e
+
+###############################################################################
+# Setup and checking for dependencies                                         #
+###############################################################################
+
+if ! command -v emcmake &> /dev/null
+then
+  echo "'emcmake' not found, setup environment according to https://emscripten.org/docs/getting_started/downloads.html"
+  exit
+fi
+
+CMAKE_BIN=${CMAKE_BIN:-$(which cmake)}
+ROOT_DIR=$(git rev-parse --show-toplevel)
+SOURCE_DIR=${ROOT_DIR}/experimental/web/sample_dynamic
+
+BUILD_DIR=${ROOT_DIR?}/build-emscripten
+mkdir -p ${BUILD_DIR}
+
+BINARY_DIR=${BUILD_DIR}/experimental/web/sample_dynamic
+mkdir -p ${BINARY_DIR}
+
+###############################################################################
+# Compile from .mlir input to portable .vmfb file using host tools            #
+###############################################################################
+
+# TODO(scotttodd): portable path ... discover from python install if on $PATH?
+INSTALL_ROOT="D:\dev\projects\iree-build\install\bin"
+TRANSLATE_TOOL="${INSTALL_ROOT?}/iree-translate.exe"
+EMBED_DATA_TOOL="${INSTALL_ROOT?}/generate_embed_data.exe"
+INPUT_NAME="simple_abs"
+INPUT_PATH="${ROOT_DIR?}/iree/samples/models/simple_abs.mlir"
+
+echo "=== Translating MLIR to Wasm VM flatbuffer output (.vmfb) ==="
+${TRANSLATE_TOOL?} ${INPUT_PATH} \
+  --iree-mlir-to-vm-bytecode-module \
+  --iree-input-type=mhlo \
+  --iree-hal-target-backends=llvm \
+  --iree-llvm-target-triple=wasm32-unknown-emscripten \
+  --iree-llvm-target-cpu-features=+atomics,+bulk-memory,+simd128 \
+  --iree-llvm-link-embedded=false \
+  --o ${BINARY_DIR}/${INPUT_NAME}.vmfb
+
+###############################################################################
+# Build the web artifacts using Emscripten                                    #
+###############################################################################
+
+echo "=== Building web artifacts using Emscripten ==="
+
+pushd ${BUILD_DIR}
+
+# Configure using Emscripten's CMake wrapper, then build.
+# Note: The sample creates a device directly, so no drivers are required.
+emcmake "${CMAKE_BIN?}" -G Ninja .. \
+  -DCMAKE_BUILD_TYPE=RelWithDebInfo \
+  -DIREE_HOST_BINARY_ROOT=$PWD/../build-host/install \
+  -DIREE_BUILD_EXPERIMENTAL_WEB_SAMPLES=ON \
+  -DIREE_HAL_DRIVER_DEFAULTS=OFF \
+  -DIREE_BUILD_COMPILER=OFF \
+  -DIREE_BUILD_TESTS=OFF
+
+"${CMAKE_BIN?}" --build . --target \
+  iree_experimental_web_sample_dynamic_sync
+
+popd
+
+###############################################################################
+# Serve the sample using a local webserver                                    #
+###############################################################################
+
+echo "=== Copying static files (.html, .js) to the build directory ==="
+
+cp ${SOURCE_DIR?}/index.html ${BINARY_DIR}
+cp ${SOURCE_DIR?}/iree_api.js ${BINARY_DIR}
+cp ${SOURCE_DIR?}/iree_worker.js ${BINARY_DIR}
+
+echo "=== Running local webserver, open at http://localhost:8000/ ==="
+
+python3 ${ROOT_DIR?}/scripts/local_web_server.py --directory ${BINARY_DIR}
diff --git a/experimental/web/sample_dynamic/device_sync.c b/experimental/web/sample_dynamic/device_sync.c
new file mode 100644
index 0000000..684b11c
--- /dev/null
+++ b/experimental/web/sample_dynamic/device_sync.c
@@ -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
+
+#include "iree/hal/local/loaders/system_library_loader.h"
+#include "iree/hal/local/sync_device.h"
+
+iree_status_t create_device_with_wasm_loader(iree_allocator_t host_allocator,
+                                             iree_hal_device_t** out_device) {
+  iree_hal_sync_device_params_t params;
+  iree_hal_sync_device_params_initialize(&params);
+
+  iree_status_t status = iree_ok_status();
+
+  iree_hal_executable_loader_t* loaders[1] = {NULL};
+  iree_host_size_t loader_count = 0;
+  if (iree_status_is_ok(status)) {
+    status = iree_hal_system_library_loader_create(
+        iree_hal_executable_import_provider_null(), host_allocator,
+        &loaders[loader_count++]);
+  }
+
+  iree_string_view_t identifier = iree_make_cstring_view("sync");
+  iree_hal_allocator_t* device_allocator = NULL;
+  if (iree_status_is_ok(status)) {
+    status = iree_hal_allocator_create_heap(identifier, host_allocator,
+                                            host_allocator, &device_allocator);
+  }
+
+  if (iree_status_is_ok(status)) {
+    status = iree_hal_sync_device_create(identifier, &params, loader_count,
+                                         loaders, device_allocator,
+                                         host_allocator, out_device);
+  }
+
+  iree_hal_allocator_release(device_allocator);
+  for (iree_host_size_t i = 0; i < loader_count; ++i) {
+    iree_hal_executable_loader_release(loaders[i]);
+  }
+  return status;
+}
diff --git a/experimental/web/sample_dynamic/index.html b/experimental/web/sample_dynamic/index.html
new file mode 100644
index 0000000..9342f94
--- /dev/null
+++ b/experimental/web/sample_dynamic/index.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<html>
+
+<!--
+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
+-->
+
+<head>
+  <meta charset="utf-8" />
+  <title>IREE Dynamic Web Sample (dlopen)</title>
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+
+  <script src="./iree_api.js"></script>
+</head>
+
+<body style="background-color: #2b2c30; color: #ABB2BF">
+  <h1>IREE Dynamic Web Sample (dlopen)</h1>
+
+  <!-- TODO(scotttodd): drag and drop UI -->
+  <!-- TODO(scotttodd): <div>s with output (or just console.log?)-->
+
+  <script>
+    let ireeInitialized = false;
+
+    // TODO(scotttodd): rewrite with async + await?
+    // TODO(scotttodd): call ireeLoadProgram after some user interaction?
+    ireeInitializeWorker().then((result) => {
+      ireeInitialized = true;
+
+      console.log("IREE initialized, loading program...");
+
+      ireeLoadProgram("./simple_abs.vmfb").then((result) => {
+        console.log("Load program success!");
+      }).catch((error) => {
+        console.error("Failed to load program, error: '" + error + "'");
+      });
+    }).catch((error) => {
+      console.error("Failed to initialize IREE, error: '" + error + "'");
+    });
+  </script>
+</body>
+
+</html>
diff --git a/experimental/web/sample_dynamic/iree_api.js b/experimental/web/sample_dynamic/iree_api.js
new file mode 100644
index 0000000..5e08e49
--- /dev/null
+++ b/experimental/web/sample_dynamic/iree_api.js
@@ -0,0 +1,71 @@
+// 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
+
+// Promise-based API for interacting with the IREE runtime.
+
+let ireeWorker = null;
+let nextMessageId = 0;
+const pendingPromises = {};
+
+// Communication protocol to and from the worker:
+// {
+//     'messageType': string
+//         * the type of message (initialized, loadProgramResult, etc.)
+//     'id': number?
+//         * optional id to disambiguate messages of the same type
+//     'payload': Object?
+//         * optional message data, format defined by message type
+//     'error': string?
+//         * optional error message
+// }
+
+function _handleMessageFromWorker(messageEvent) {
+  const {messageType, id, payload, error} = messageEvent.data;
+
+  if (messageType == 'initialized') {
+    pendingPromises['initialize']['resolve']();
+    delete pendingPromises['initialize'];
+  } else if (messageType == 'loadProgramResult') {
+    if (error !== undefined) {
+      pendingPromises[id]['reject'](error);
+    } else {
+      pendingPromises[id]['resolve'](payload);
+    }
+    delete pendingPromises[id];
+  }
+}
+
+// Initializes IREE's web worker asynchronously.
+// Resolves when the worker is fully initialized.
+function ireeInitializeWorker() {
+  return new Promise((resolve, reject) => {
+    pendingPromises['initialize'] = {
+      'resolve': resolve,
+      'reject': reject,
+    };
+
+    ireeWorker = new Worker('iree_worker.js', {name: 'IREE-main'});
+    ireeWorker.onmessage = _handleMessageFromWorker;
+  });
+}
+
+function ireeLoadProgram(vmfbPath) {
+  return new Promise((resolve, reject) => {
+    const messageId = nextMessageId++;
+    const message = {
+      'messageType': 'loadProgram',
+      'id': messageId,
+      'payload': vmfbPath,
+    };
+
+    pendingPromises[messageId] = {
+      'resolve': resolve,
+      'reject': reject,
+    };
+
+    ireeWorker.postMessage(message);
+  });
+}
diff --git a/experimental/web/sample_dynamic/iree_worker.js b/experimental/web/sample_dynamic/iree_worker.js
new file mode 100644
index 0000000..de4ecb7
--- /dev/null
+++ b/experimental/web/sample_dynamic/iree_worker.js
@@ -0,0 +1,82 @@
+// 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): configure this through the build system / scripts?
+// const MAIN_SCRIPT_URL = 'web-sample-dynamic-multithreaded.js';
+const MAIN_SCRIPT_URL = 'web-sample-dynamic-sync.js';
+
+let wasmLoadProgramFn;
+var Module = {
+  print: function(text) {
+    console.log('(C)', text);
+  },
+  printErr: function(text) {
+    console.error('(C)', text);
+  },
+  onRuntimeInitialized: function() {
+    console.log('WebAssembly module onRuntimeInitialized()');
+
+    wasmLoadProgramFn =
+        Module.cwrap('load_program', 'number', ['number', 'number']);
+
+    postMessage({
+      'messageType': 'initialized',
+    });
+  },
+  noInitialRun: true,
+};
+
+function loadProgram(id, vmfbPath) {
+  console.log('fetching program at \'%s\'', vmfbPath);
+
+  const fetchRequest = new XMLHttpRequest();
+
+  fetchRequest.onload = function(progressEvent) {
+    console.log('XMLHttpRequest completed, passing to Wasm module');
+
+    const programDataBuffer = progressEvent.target.response;
+    const programDataView = new Int8Array(programDataBuffer);
+
+    const programDataWasmBuffer = Module._malloc(
+        programDataView.length * programDataView.BYTES_PER_ELEMENT);
+    Module.HEAP8.set(programDataView, programDataWasmBuffer);
+
+    const result =
+        wasmLoadProgramFn(programDataWasmBuffer, programDataBuffer.byteLength);
+    console.log('Result from loadProgramFn():', result);
+    Module._free(programDataWasmBuffer);
+
+    if (result !== 0) {
+      postMessage({
+        'messageType': 'loadProgramResult',
+        'id': id,
+        'error': 'Wasm module error, check console for details',
+      });
+    } else {
+      postMessage({
+        'messageType': 'loadProgramResult',
+        'id': id,
+        'payload': 'success',
+      });
+    }
+  };
+
+  fetchRequest.open('GET', vmfbPath);
+  fetchRequest.responseType = 'arraybuffer';
+  fetchRequest.send();
+}
+
+self.onmessage = function(messageEvent) {
+  const {messageType, id, payload} = messageEvent.data;
+
+  console.log('worker received message:', messageEvent.data);
+
+  if (messageType == 'loadProgram') {
+    loadProgram(id, payload);
+  }
+};
+
+importScripts(MAIN_SCRIPT_URL);
diff --git a/experimental/web/sample_dynamic/main.c b/experimental/web/sample_dynamic/main.c
new file mode 100644
index 0000000..460842d
--- /dev/null
+++ b/experimental/web/sample_dynamic/main.c
@@ -0,0 +1,162 @@
+// 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 <stdint.h>
+#include <stdio.h>
+
+#include "iree/runtime/api.h"
+#include "iree/vm/bytecode_module.h"
+
+//===----------------------------------------------------------------------===//
+// Public API
+//===----------------------------------------------------------------------===//
+
+int load_program(uint8_t* vmfb_data, size_t length);
+
+//===----------------------------------------------------------------------===//
+// Implementation
+//===----------------------------------------------------------------------===//
+
+// We're not really using cpuinfo here, but the linker complains about this
+// function not being defined. It actually is defined though, _if_ you patch
+// cpuinfo's CMake configuration: https://github.com/pytorch/cpuinfo/issues/34.
+void __attribute__((weak)) cpuinfo_emscripten_init(void) {}
+
+extern iree_status_t create_device_with_wasm_loader(
+    iree_allocator_t host_allocator, iree_hal_device_t** out_device);
+
+void inspect_module(iree_vm_module_t* module) {
+  fprintf(stdout, "=== module properties ===\n");
+
+  iree_string_view_t module_name = iree_vm_module_name(module);
+  fprintf(stdout, "  module name: '%.*s'\n", (int)module_name.size,
+          module_name.data);
+
+  iree_vm_module_signature_t module_signature =
+      iree_vm_module_signature(module);
+  fprintf(stdout, "  module signature:\n");
+  fprintf(stdout, "    %" PRIhsz " imported functions\n",
+          module_signature.import_function_count);
+  fprintf(stdout, "    %" PRIhsz " exported functions\n",
+          module_signature.export_function_count);
+  fprintf(stdout, "    %" PRIhsz " internal functions\n",
+          module_signature.internal_function_count);
+
+  fprintf(stdout, "  exported functions:\n");
+  for (iree_host_size_t i = 0; i < module_signature.export_function_count;
+       ++i) {
+    iree_vm_function_t function;
+    iree_status_t status = iree_vm_module_lookup_function_by_ordinal(
+        module, IREE_VM_FUNCTION_LINKAGE_EXPORT, i, &function);
+
+    iree_string_view_t function_name = iree_vm_function_name(&function);
+    iree_vm_function_signature_t function_signature =
+        iree_vm_function_signature(&function);
+    iree_string_view_t calling_convention =
+        function_signature.calling_convention;
+    fprintf(stdout, "    function name: '%.*s', calling convention: %.*s'\n",
+            (int)function_name.size, function_name.data,
+            (int)calling_convention.size, calling_convention.data);
+  }
+}
+
+int load_program(uint8_t* vmfb_data, size_t length) {
+  fprintf(stdout, "load_program() received %zu bytes of data\n", length);
+
+  iree_runtime_instance_options_t instance_options;
+  iree_runtime_instance_options_initialize(IREE_API_VERSION_LATEST,
+                                           &instance_options);
+  // Note: no call to iree_runtime_instance_options_use_all_available_drivers().
+
+  iree_runtime_instance_t* instance = NULL;
+  iree_status_t status = iree_runtime_instance_create(
+      &instance_options, iree_allocator_system(), &instance);
+
+  iree_hal_device_t* device = NULL;
+  if (iree_status_is_ok(status)) {
+    status = create_device_with_wasm_loader(iree_allocator_system(), &device);
+  }
+
+  iree_runtime_session_options_t session_options;
+  iree_runtime_session_options_initialize(&session_options);
+  iree_runtime_session_t* session = NULL;
+  if (iree_status_is_ok(status)) {
+    status = iree_runtime_session_create_with_device(
+        instance, &session_options, device,
+        iree_runtime_instance_host_allocator(instance), &session);
+  }
+
+  iree_vm_module_t* program_module = NULL;
+  if (iree_status_is_ok(status)) {
+    status = iree_vm_bytecode_module_create(
+        iree_make_const_byte_span(vmfb_data, length), iree_allocator_null(),
+        iree_allocator_system(), &program_module);
+  }
+
+  if (iree_status_is_ok(status)) {
+    inspect_module(program_module);
+    status = iree_runtime_session_append_module(session, program_module);
+  }
+
+  // Call the 'abs' function in the module.
+  iree_runtime_call_t call;
+  if (iree_status_is_ok(status)) {
+    status = iree_runtime_call_initialize_by_name(
+        session, iree_make_cstring_view("module.abs"), &call);
+  }
+
+  iree_hal_buffer_view_t* arg0 = NULL;
+  float arg0_data[1] = {-5.5};
+  if (iree_status_is_ok(status)) {
+    status = iree_hal_buffer_view_allocate_buffer(
+        iree_runtime_session_device_allocator(session), /*shape=*/NULL,
+        /*shape_rank=*/0, IREE_HAL_ELEMENT_TYPE_FLOAT_32,
+        IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR,
+        IREE_HAL_MEMORY_TYPE_HOST_LOCAL | IREE_HAL_MEMORY_TYPE_DEVICE_VISIBLE,
+        IREE_HAL_BUFFER_USAGE_DISPATCH | IREE_HAL_BUFFER_USAGE_TRANSFER,
+        iree_make_const_byte_span((void*)arg0_data, sizeof(arg0_data)), &arg0);
+  }
+  if (iree_status_is_ok(status)) {
+    status = iree_runtime_call_inputs_push_back_buffer_view(&call, arg0);
+  }
+  iree_hal_buffer_view_release(arg0);
+
+  if (iree_status_is_ok(status)) {
+    status = iree_runtime_call_invoke(&call, /*flags=*/0);
+  }
+
+  iree_hal_buffer_view_t* ret_buffer_view = NULL;
+  if (iree_status_is_ok(status)) {
+    status = iree_runtime_call_outputs_pop_front_buffer_view(&call,
+                                                             &ret_buffer_view);
+  }
+  float result[1] = {0.0f};
+  if (iree_status_is_ok(status)) {
+    status =
+        iree_hal_buffer_read_data(iree_hal_buffer_view_buffer(ret_buffer_view),
+                                  0, result, sizeof(result));
+  }
+  iree_hal_buffer_view_release(ret_buffer_view);
+
+  if (iree_status_is_ok(status)) {
+    fprintf(stdout, "abs(%f) -> result: %f\n", arg0_data[0], result[0]);
+  }
+
+  iree_runtime_call_deinitialize(&call);
+
+  iree_vm_module_release(program_module);
+  iree_hal_device_release(device);
+  iree_runtime_session_release(session);
+  iree_runtime_instance_release(instance);
+
+  if (!iree_status_is_ok(status)) {
+    iree_status_fprint(stderr, status);
+    iree_status_free(status);
+    return -1;
+  }
+
+  return 0;
+}
diff --git a/experimental/sample_web_static/CMakeLists.txt b/experimental/web/sample_static/CMakeLists.txt
similarity index 89%
rename from experimental/sample_web_static/CMakeLists.txt
rename to experimental/web/sample_static/CMakeLists.txt
index d4c7a12..494cb6b 100644
--- a/experimental/sample_web_static/CMakeLists.txt
+++ b/experimental/web/sample_static/CMakeLists.txt
@@ -8,7 +8,12 @@
   return()
 endif()
 
-set(_MNIST_OBJECT_NAME "iree_experimental_sample_web_static_mnist")
+if(NOT EXISTS "${CMAKE_CURRENT_BINARY_DIR}/mnist_static.h")
+  message(WARNING "Missing mnist_static.h, run ${CMAKE_CURRENT_SOURCE_DIR}/build_sample.sh to generate it")
+  return()
+endif()
+
+set(_MNIST_OBJECT_NAME "iree_experimental_web_sample_static_mnist")
 add_library(${_MNIST_OBJECT_NAME} STATIC ${CMAKE_CURRENT_BINARY_DIR}/mnist_static.o)
 SET_TARGET_PROPERTIES(${_MNIST_OBJECT_NAME} PROPERTIES LINKER_LANGUAGE C)
 
@@ -16,7 +21,7 @@
 # Sync
 #-------------------------------------------------------------------------------
 
-set(_NAME "iree_experimental_sample_web_static_sync")
+set(_NAME "iree_experimental_web_sample_static_sync")
 add_executable(${_NAME} "")
 target_include_directories(${_NAME} PUBLIC
     $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>
@@ -29,7 +34,7 @@
     ${CMAKE_CURRENT_BINARY_DIR}/mnist_bytecode.h
     ${CMAKE_CURRENT_BINARY_DIR}/mnist_bytecode.c
 )
-set_target_properties(${_NAME} PROPERTIES OUTPUT_NAME "sample-web-static-sync")
+set_target_properties(${_NAME} PROPERTIES OUTPUT_NAME "web-sample-static-sync")
 
 # Note: we have to be very careful about dependencies here.
 #
@@ -58,7 +63,7 @@
 # Multithreaded
 #-------------------------------------------------------------------------------
 
-set(_NAME "iree_experimental_sample_web_static_multithreaded")
+set(_NAME "iree_experimental_web_sample_static_multithreaded")
 add_executable(${_NAME} "")
 target_include_directories(${_NAME} PUBLIC
     $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>
@@ -71,7 +76,7 @@
     ${CMAKE_CURRENT_BINARY_DIR}/mnist_bytecode.h
     ${CMAKE_CURRENT_BINARY_DIR}/mnist_bytecode.c
 )
-set_target_properties(${_NAME} PROPERTIES OUTPUT_NAME "sample-web-static-multithreaded")
+set_target_properties(${_NAME} PROPERTIES OUTPUT_NAME "web-sample-static-multithreaded")
 
 # Note: we have to be very careful about dependencies here.
 #
diff --git a/experimental/sample_web_static/README.md b/experimental/web/sample_static/README.md
similarity index 78%
rename from experimental/sample_web_static/README.md
rename to experimental/web/sample_static/README.md
index 8fceff1..7360d3b 100644
--- a/experimental/sample_web_static/README.md
+++ b/experimental/web/sample_static/README.md
@@ -4,25 +4,28 @@
 IREE. The output artifact is a web page containing an interactive MNIST digits
 classifier.
 
+The MNIST ML model is compiled statically together with the IREE runtime into
+a single .js + .wasm bundle.
+
 ## Quickstart
 
 1. Install IREE's host tools (e.g. by building the `install` target with CMake)
 2. Install the Emscripten SDK by
    [following these directions](https://emscripten.org/docs/getting_started/downloads.html)
 3. Initialize your Emscripten environment (e.g. run `emsdk_env.bat`)
-4. From this directory, run `bash ./build_static_emscripten_demo.sh`
+4. From this directory, run `bash ./build_sample.sh`
     * You may need to set the path to your host tools install
 5. Open the localhost address linked in the script output
 
-To rebuild most parts of the demo (C runtime, sample HTML, CMake config, etc.),
-just `control + C` to stop the local webserver and rerun the script.
+To rebuild most parts of the sample (C runtime, sample HTML, CMake config,
+etc.), just `control + C` to stop the local webserver and rerun the script.
 
 ## How it works
 
-This [MNIST model](../../iree/samples/models/mnist.mlir), also used in the
-[Vision sample](../../iree/samples/vision/), is compiled using the "static
+This [MNIST model](../../../iree/samples/models/mnist.mlir), also used in the
+[Vision sample](../../../iree/samples/vision/), is compiled using the "static
 library" output setting of IREE's compiler (see the
-[Static library sample](../../iree/samples/static_library)). The resulting
+[Static library sample](../../../iree/samples/static_library)). The resulting
 `.h` and `.o` files are compiled together with `main.c`, while the `.vmfb` is
 embedded into a C file that is similarly linked in.
 
diff --git a/experimental/sample_web_static/build_static_emscripten_demo.sh b/experimental/web/sample_static/build_sample.sh
similarity index 83%
rename from experimental/sample_web_static/build_static_emscripten_demo.sh
rename to experimental/web/sample_static/build_sample.sh
index 3c16a34..51665ae 100644
--- a/experimental/sample_web_static/build_static_emscripten_demo.sh
+++ b/experimental/web/sample_static/build_sample.sh
@@ -19,11 +19,12 @@
 
 CMAKE_BIN=${CMAKE_BIN:-$(which cmake)}
 ROOT_DIR=$(git rev-parse --show-toplevel)
+SOURCE_DIR=${ROOT_DIR}/experimental/web/sample_static
 
 BUILD_DIR=${ROOT_DIR?}/build-emscripten
 mkdir -p ${BUILD_DIR}
 
-BINARY_DIR=${BUILD_DIR}/experimental/sample_web_static/
+BINARY_DIR=${BUILD_DIR}/experimental/web/sample_static/
 mkdir -p ${BINARY_DIR}
 
 ###############################################################################
@@ -62,35 +63,33 @@
 
 echo "=== Building web artifacts using Emscripten ==="
 
-pushd ${ROOT_DIR?}/build-emscripten
+pushd ${BUILD_DIR}
 
 # Configure using Emscripten's CMake wrapper, then build.
-# Note: The sample creates a task device directly, so no drivers are required,
-#       but some targets are gated on specific CMake options.
+# Note: The sample creates a device directly, so no drivers are required.
 emcmake "${CMAKE_BIN?}" -G Ninja .. \
   -DCMAKE_BUILD_TYPE=RelWithDebInfo \
   -DIREE_HOST_BINARY_ROOT=$PWD/../build-host/install \
   -DIREE_BUILD_EXPERIMENTAL_WEB_SAMPLES=ON \
   -DIREE_HAL_DRIVER_DEFAULTS=OFF \
-  -DIREE_HAL_DRIVER_DYLIB=ON \
   -DIREE_BUILD_COMPILER=OFF \
   -DIREE_BUILD_TESTS=OFF
 
 "${CMAKE_BIN?}" --build . --target \
-    iree_experimental_sample_web_static_sync \
-    iree_experimental_sample_web_static_multithreaded
+    iree_experimental_web_sample_static_sync \
+    iree_experimental_web_sample_static_multithreaded
 
 popd
 
 ###############################################################################
-# Serve the demo using a local webserver                                      #
+# Serve the sample using a local webserver                                    #
 ###############################################################################
 
 echo "=== Copying static files to the build directory ==="
 
-cp ${ROOT_DIR?}/experimental/sample_web_static/index.html ${BINARY_DIR}
-cp ${ROOT_DIR?}/experimental/sample_web_static/iree_api.js ${BINARY_DIR}
-cp ${ROOT_DIR?}/experimental/sample_web_static/iree_worker.js ${BINARY_DIR}
+cp ${SOURCE_DIR}/index.html ${BINARY_DIR}
+cp ${SOURCE_DIR}/iree_api.js ${BINARY_DIR}
+cp ${SOURCE_DIR}/iree_worker.js ${BINARY_DIR}
 
 EASELJS_LIBRARY=${BINARY_DIR}/easeljs.min.js
 test -f ${EASELJS_LIBRARY} || \
diff --git a/experimental/sample_web_static/device_multithreaded.c b/experimental/web/sample_static/device_multithreaded.c
similarity index 100%
rename from experimental/sample_web_static/device_multithreaded.c
rename to experimental/web/sample_static/device_multithreaded.c
diff --git a/experimental/sample_web_static/device_sync.c b/experimental/web/sample_static/device_sync.c
similarity index 100%
rename from experimental/sample_web_static/device_sync.c
rename to experimental/web/sample_static/device_sync.c
diff --git a/experimental/sample_web_static/index.html b/experimental/web/sample_static/index.html
similarity index 100%
rename from experimental/sample_web_static/index.html
rename to experimental/web/sample_static/index.html
diff --git a/experimental/sample_web_static/iree_api.js b/experimental/web/sample_static/iree_api.js
similarity index 100%
rename from experimental/sample_web_static/iree_api.js
rename to experimental/web/sample_static/iree_api.js
diff --git a/experimental/sample_web_static/iree_worker.js b/experimental/web/sample_static/iree_worker.js
similarity index 96%
rename from experimental/sample_web_static/iree_worker.js
rename to experimental/web/sample_static/iree_worker.js
index 77b27fc..7800669 100644
--- a/experimental/sample_web_static/iree_worker.js
+++ b/experimental/web/sample_static/iree_worker.js
@@ -17,8 +17,8 @@
 }
 
 // TODO(scotttodd): configure this through the build system / scripts?
-const MAIN_SCRIPT_URL = 'sample-web-static-multithreaded.js';
-// const MAIN_SCRIPT_URL = 'sample-web-static-sync.js';
+const MAIN_SCRIPT_URL = 'web-sample-static-multithreaded.js';
+// const MAIN_SCRIPT_URL = 'web-sample-static-sync.js';
 
 let wasmSetupSampleFn;
 let wasmCleanupSampleFn;
diff --git a/experimental/sample_web_static/main.c b/experimental/web/sample_static/main.c
similarity index 98%
rename from experimental/sample_web_static/main.c
rename to experimental/web/sample_static/main.c
index 23441bf..64af5fc 100644
--- a/experimental/sample_web_static/main.c
+++ b/experimental/web/sample_static/main.c
@@ -16,7 +16,6 @@
 //===----------------------------------------------------------------------===//
 
 typedef struct iree_sample_state_t iree_sample_state_t;
-static void iree_sample_state_initialize(iree_sample_state_t* out_state);
 
 // TODO(scotttodd): figure out error handling and state management
 //     * out_state and return status would make sense, but emscripten...
diff --git a/iree/base/internal/dynamic_library_posix.c b/iree/base/internal/dynamic_library_posix.c
index 73948f6..b72b1f0 100644
--- a/iree/base/internal/dynamic_library_posix.c
+++ b/iree/base/internal/dynamic_library_posix.c
@@ -15,7 +15,7 @@
 #include "iree/base/tracing.h"
 
 #if defined(IREE_PLATFORM_ANDROID) || defined(IREE_PLATFORM_APPLE) || \
-    defined(IREE_PLATFORM_LINUX)
+    defined(IREE_PLATFORM_LINUX) || defined(IREE_PLATFORM_EMSCRIPTEN)
 
 #include <dlfcn.h>
 #include <errno.h>
diff --git a/iree/compiler/Dialect/HAL/Target/LLVM/internal/WasmLinkerTool.cpp b/iree/compiler/Dialect/HAL/Target/LLVM/internal/WasmLinkerTool.cpp
index 81807ba..9a6a32c 100644
--- a/iree/compiler/Dialect/HAL/Target/LLVM/internal/WasmLinkerTool.cpp
+++ b/iree/compiler/Dialect/HAL/Target/LLVM/internal/WasmLinkerTool.cpp
@@ -86,9 +86,17 @@
         // Treat warnings as errors.
         "--fatal-warnings",
 
-        // Generated a shared object, not an executable.
-        // Note: disabled since creating shared libraries is not yet supported.
-        // "--shared",
+        // Generated a shared object containing position-independent-code.
+        "--experimental-pic",
+        "--shared",
+
+        // Import [shared] memory from the environment.
+        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Memory#creating_a_shared_memory
+        // TODO(scotttodd): Add a flag controlling these - some combination is
+        //   required when using multithreading + SharedArrayBuffer, but they
+        //   must be left off when running single threaded.
+        // "--import-memory",
+        // "--shared-memory",
 
         "-o " + artifacts.libraryFile.path,
     };
diff --git a/iree/hal/local/BUILD b/iree/hal/local/BUILD
index 26bcff7..1d1b786 100644
--- a/iree/hal/local/BUILD
+++ b/iree/hal/local/BUILD
@@ -115,7 +115,9 @@
 iree_cmake_extra_content(
     content = """
 # task_driver is used by asynchronuous drivers.
-if(NOT (${IREE_HAL_DRIVER_DYLIB} OR ${IREE_HAL_DRIVER_VMVX}))
+# TODO(scotttodd): refactor this - code depending on threading should be
+#   possible to declare in the build system but conditionally link in
+if(NOT EMSCRIPTEN AND NOT (${IREE_HAL_DRIVER_DYLIB} OR ${IREE_HAL_DRIVER_VMVX}))
   return()
 endif()
 """,
diff --git a/iree/hal/local/CMakeLists.txt b/iree/hal/local/CMakeLists.txt
index b28fcd8..0befb9c 100644
--- a/iree/hal/local/CMakeLists.txt
+++ b/iree/hal/local/CMakeLists.txt
@@ -105,7 +105,9 @@
 )
 
 # task_driver is used by asynchronuous drivers.
-if(NOT (${IREE_HAL_DRIVER_DYLIB} OR ${IREE_HAL_DRIVER_VMVX}))
+# TODO(scotttodd): refactor this - code depending on threading should be
+#   possible to declare in the build system but conditionally link in
+if(NOT EMSCRIPTEN AND NOT (${IREE_HAL_DRIVER_DYLIB} OR ${IREE_HAL_DRIVER_VMVX}))
   return()
 endif()
 
diff --git a/iree/hal/local/loaders/system_library_loader.c b/iree/hal/local/loaders/system_library_loader.c
index f4cf909..d327eaa 100644
--- a/iree/hal/local/loaders/system_library_loader.c
+++ b/iree/hal/local/loaders/system_library_loader.c
@@ -413,6 +413,8 @@
 #define IREE_PLATFORM_DYLIB_TYPE "dylib"
 #elif defined(IREE_PLATFORM_WINDOWS)
 #define IREE_PLATFORM_DYLIB_TYPE "dll"
+#elif defined(IREE_PLATFORM_EMSCRIPTEN)
+#define IREE_PLATFORM_DYLIB_TYPE "wasm"
 #else
 #define IREE_PLATFORM_DYLIB_TYPE "elf"
 #endif  // IREE_PLATFORM_*