Moving existing custom_module sample to custom_module/basic/.
diff --git a/samples/custom_module/CMakeLists.txt b/samples/custom_module/CMakeLists.txt
index dbfb002..1e70c9d 100644
--- a/samples/custom_module/CMakeLists.txt
+++ b/samples/custom_module/CMakeLists.txt
@@ -4,27 +4,4 @@
 # See https://llvm.org/LICENSE.txt for license information.
 # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 
-set(_NAME "iree_samples_custom_module_run")
-add_executable(${_NAME} "")
-target_sources(${_NAME}
-  PRIVATE
-    main.c
-    module.cc
-    module.h
-)
-
-set_target_properties(${_NAME} PROPERTIES OUTPUT_NAME "custom-module-run")
-
-# TODO(benvanik): make iree_status_annotate_f always available as a function
-# instead of defining it empty? otherwise optimized builds of the runtime won't
-# export it but external libraries may pull it in.
-target_compile_options(${_NAME} PRIVATE ${IREE_DEFAULT_COPTS})
-
-target_link_libraries(${_NAME}
-  iree_base_base
-  iree_base_internal_file_io
-  iree_vm_vm
-  iree_vm_bytecode_module
-)
-
-add_subdirectory(test)
+iree_add_all_subdirs()
diff --git a/samples/custom_module/README.md b/samples/custom_module/README.md
index bece021..5b603ed 100644
--- a/samples/custom_module/README.md
+++ b/samples/custom_module/README.md
@@ -1,261 +1,15 @@
-# "Custom Module" sample
+# "Custom Module" samples
 
-This sample shows how to
+These samples demonstrate how to extend IREE with custom host code that can be
+called from compiled modules. All modules regardless of type can call into each
+other to allow for arbitrary module configurations.
 
-1. Create a custom module in C++ that can be used with the IREE runtime
-2. Author an MLIR input that uses a custom module including a custom type
-3. Compile that program to an IREE VM bytecode module
-4. Load the compiled program using a low-level VM interface
-5. Call exported functions on the loaded program to exercise the custom module
+## Basic sample
 
-The custom module is declared in [`module.h`](./module.h), implemented using a
-C++ module wrapper layer in [`module.cc`](./module.cc), and called by example in
-[`main.c`](./main.c).
+[samples/custom_module/basic/](/samples/custom_module/basic/README.md) shows how
+to add a basic C++ custom module and use many of the more advanced features of
+the module system.
 
-This document uses terminology that can be found in the documentation of
-[IREE's execution model](https://github.com/iree-org/iree/blob/main/docs/developers/design_docs/execution_model.md).
-See [IREE's extensibility mechanisms](https://iree-org.github.io/iree/extensions/)
-documentation for more information specific to extenting IREE and
-alternative approaches to doing so.
-
-## Background
-
-IREE's VM is used to dynamically link modules of various types together at
-runtime (C, C++, IREE's VM bytecode, etc). Via this mechanism any number of
-modules containing exported functions and types that can be used across modules
-can extend IREE's base functionality. In most IREE programs the HAL module is
-used to provide a hardware abstraction layer for execution and both the HAL
-module itself and the types it exposes (`!hal.buffer`, `!hal.executable`, etc)
-are implemented using this mechanism.
-
-## Instructions
-
-1. Build or install the `iree-compile` binary:
-
-    ```
-    python -m pip install iree-compiler
-    ```
-
-    [See here](https://iree-org.github.io/iree/getting-started/)
-    for general instructions on installing the compiler.
-
-3. Compile the [example module](./test/example.mlir) to a .vmfb file:
-
-    ```
-    # This simple sample doesn't use tensors and can be compiled in host-only
-    # mode to avoid the need for the HAL.
-    iree-compile --iree-execution-model=host-only samples/custom_module/test/example.mlir -o=/tmp/example.vmfb
-    ```
-
-3. Build the `iree_samples_custom_module_run` CMake target :
-
-    ```
-    cmake -B ../iree-build/ -DCMAKE_BUILD_TYPE=RelWithDebInfo . \
-        -DCMAKE_C_FLAGS=-DIREE_VM_EXECUTION_TRACING_FORCE_ENABLE=1
-    cmake --build ../iree-build/ --target iree_samples_custom_module_run
-    ```
-    (here we force runtime execution tracing for demonstration purposes)
-
-    [See here](https://iree-org.github.io/iree/building-from-source/getting-started/)
-    for general instructions on building using CMake.
-
-4. Run the example program to call the main function:
-
-   ```
-   ../iree-build/samples/custom_module/custom-module-run \
-       /tmp/example.vmfb example.main
-   ```
-
-## Defining Custom Modules in C++
-
-Modules are exposed to applications and the IREE VM via the `iree_vm_module_t`
-interface. IREE canonically uses C headers to expose module and type functions
-but the implementation of the module can be anything the user is able to work
-with (C, C++, rust, etc).
-
-A C++ wrapper is provided to ease implementation when minimal code size and overhead is not a focus and provides easy definition of exports and marshaling
-of types. Utilities such as `iree::Status` and `iree::vm::ref<T>` add safety for
-managing reference counted resources and can be used within the modules.
-
-General flow:
-
-1. Expose module via a C API ([`module.h`](./module.h)):
-
-```c
-// Ideally all allocations performed by the module should use |allocator|.
-// The returned module in |out_module| should have a ref count of 1 to transfer
-// ownership to the caller.
-iree_status_t iree_table_module_create(iree_allocator_t allocator,
-                                       iree_vm_module_t** out_module);
-```
-
-2. Implement the module using C/C++/etc ([`module.cc`](./module.cc)):
-
-Modules have two parts: a shared module and instantiated state.
-
-The `iree::vm::NativeModule` helper is used to handle the shared module
-declaration and acts as a factory for per-context instantiated state and the
-methods exported by the module:
-
-```c++
-// Any mutable state stored on the module may be accessed from multiple threads
-// if the module is instantiated in multiple contexts and must be thread-safe.
-struct TableModule final : public vm::NativeModule<TableModuleState> {
-  // Each time the module is instantiated this will be called to allocate the
-  // context-specific state. The returned state must only be thread-compatible
-  // as invocations within a context will not be made from multiple threads but
-  // the thread on which they are made may change over time; this means no TLS!
-  StatusOr<std::unique_ptr<TableModuleState>> CreateState(
-      iree_allocator_t allocator) override;
-};
-```
-
-The module implementation is done on the state object so that methods may use
-`this` to access context-local state:
-
-```c++
-struct TableModuleState final {
-  // Local to the context the module was instantiated in and thread-compatible.
-  std::unordered_map<std::string, std::string> mutable_state;
-
-  // Exported functions must return Status or StatusOr. Failures will result in
-  // program termination and will be propagated up to the top-level invoker.
-  // If a module wants to provide non-fatal errors it can return results to the
-  // program: here we return a 0/1 indicating whether the key was found as well
-  // as the result or null.
-  //
-  // MLIR declaration:
-  //   func.func private @table.lookup(!util.buffer) -> (i1, !util.buffer)
-  StatusOr<std::tuple<int32_t, vm::ref<iree_vm_buffer_t>>> Lookup(
-      const vm::ref<iree_vm_buffer_t> key);
-};
-```
-
-Finally the exported methods are registered and marshaling code is expanded:
-
-```c++
-static const vm::NativeFunction<TableModuleState> kTableModuleFunctions[] = {
-    vm::MakeNativeFunction("lookup", &TableModuleState::Lookup),
-};
-extern "C" iree_status_t iree_table_module_create(
-    iree_allocator_t allocator, iree_vm_module_t** out_module) {
-  auto module = std::make_unique<TableModule>(
-      "table", /*version=*/0, allocator,
-      iree::span<const vm::NativeFunction<CustomModuleState>>
-      (kTableModuleFunctions));
-  *out_module = module.release()->interface();
-  return iree_ok_status();
-}
-```
-
-## Registering Custom Modules at Runtime
-
-Once a custom module is defined it needs to be provided to any context that it
-is going to be used in. Each context may have its own unique mix of modules and
-it's the hosting application's responsibility to inject the available modules.
-See [`main.c`](./main.c) for an example showing the entire end-to-end lifetime
-of loading a compiled bytecode module and providing a custom module for runtime
-dynamic linking.
-
-Since modules themselves can be reused across contexts it can be a way of
-creating shared caches (requires thread-safety!) that span contexts while the
-module state is context specific and isolated.
-
-Import resolution happens in reverse registration order: the most recently
-registered modules override previous ones. This combined with optional imports
-allows overriding behavior and version compatibility shims (though there is
-still some trickiness involved).
-
-```c
-// Ensure custom types are registered before loading modules that use them.
-// This only needs to be done once per instance.
-IREE_CHECK_OK(iree_custom_module_register_types(instance));
-
-// Create the custom module that can be reused across contexts.
-iree_vm_module_t* custom_module = NULL;
-IREE_CHECK_OK(iree_custom_module_create(instance, allocator, &custom_module));
-
-// Create the context for this invocation reusing the loaded modules.
-// Contexts hold isolated state and can be reused for multiple calls.
-// Note that the module order matters: the input user module is dependent on
-// the custom module.
-iree_vm_module_t* modules[] = {custom_module, bytecode_module};
-iree_vm_context_t* context = NULL;
-IREE_CHECK_OK(iree_vm_context_create_with_modules(
-    instance, IREE_VM_CONTEXT_FLAG_NONE, IREE_ARRAYSIZE(modules), modules,
-    allocator, &context));
-```
-
-## Calling Custom Modules from Compiled Programs
-
-The IREE compiler allows for external functions that are resolved at runtime
-using the [MLIR `func` dialect](https://mlir.llvm.org/docs/Dialects/Func/). Some
-optional attributes are used to allow for customization where required but in
-many cases no additional IREE-specific work is required in the compiled program.
-A few advanced features of the VM FFI are not currently exposed via this
-mechanism such as variadic arguments and tuples but the advantage is that users
-need not customize the IREE compiler in order to use their modules.
-
-Prior to passing input programs to the IREE compiler users can insert the
-imported functions as external
-[`func.func`](https://mlir.llvm.org/docs/Dialects/Func/#funcfunc-mlirfuncfuncop)
-ops and calls to those functions using
-[`func.call`](https://mlir.llvm.org/docs/Dialects/Func/#funccall-mlirfunccallop):
-
-```mlir
-// An external function declaration.
-// `custom` is the runtime module and `string.create` is the exported method.
-// This call uses both IREE types (`!util.buffer`) and custom ones not known to
-// the compiler but available at runtime (`!custom.string`).
-func.func private @custom.string.create(!util.buffer) -> !custom.string
-```
-
-```mlir
-// Call the imported function.
-%buffer = util.buffer.constant : !util.buffer = "hello world!"
-%result = func.call @custom.string.create(%buffer) : (!util.buffer) -> !custom.string
-```
-
-Users with custom dialects and ops can use
-[MLIR's dialect conversion](https://mlir.llvm.org/docs/DialectConversion/)
-framework to rewrite their custom ops to this form and perform additional
-marshaling logic. For example, the above could have started as this program
-before the user ran their dialect conversion and passed it in to `iree-compile`:
-
-```mlir
-%result = custom.string.create "hello world!" : !custom.string
-```
-
-See this samples [`example.mlir`](./test/example.mlir) for examples of features
-such as signature specification and optional import fallback support.
-
-### Interoperating with the HAL
-
-Currently only HAL types with synchronous behavior are supported in custom
-modules but deeper integration with the HAL is planned.
-This means that today all custom module operations are executed on the host and
-synchronous with other host behavior. Future extensions will allow for custom
-device-specific command buffer operations and asynchronous scheduling that fit
-within the
-[IREE execution model](https://github.com/iree-org/iree/blob/main/docs/developers/design_docs/execution_model.md).
-
-The same ABI mechanisms used to call in to IREE with HAL resources can be used
-to call in to custom modules:
-
-```mlir
-// Perform work on tensors using tensor dialects. This will run using IREE's
-// asynchronous execution model.
-%src = call @produce_tensor() : () -> tensor<?x3xf32>
-
-// Make the custom call synchronously on the host. The call can use the
-// `iree_hal_buffer_view_t` APIs to access the metadata and contents and
-// allocate a new buffer view to return to the program.
-%dst = func.call @custom.process(%src) : (tensor<?x3xf32>) -> tensor<3x?xi32>
-
-// Continue using the tensor as normal.
-call @consume_tensor(%dst) : (tensor<3x?xi32>) -> ()
-```
-
-Future extensions will allow for `!hal.fence` and `!hal.buffer` to be used to
-allow for chaining asynchronous submissions and assign storage for in-place
-operations.
+* C++ VM wrappers for defining modules and using reference types
+* Weak imports/fallback functions
+* Custom types exposed to the compiler
diff --git a/samples/custom_module/basic/CMakeLists.txt b/samples/custom_module/basic/CMakeLists.txt
new file mode 100644
index 0000000..ab87cf4
--- /dev/null
+++ b/samples/custom_module/basic/CMakeLists.txt
@@ -0,0 +1,30 @@
+# 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(_NAME "iree_samples_custom_module_basic_run")
+add_executable(${_NAME} "")
+target_sources(${_NAME}
+  PRIVATE
+    main.c
+    module.cc
+    module.h
+)
+
+set_target_properties(${_NAME} PROPERTIES OUTPUT_NAME "custom-module-basic-run")
+
+# TODO(benvanik): make iree_status_annotate_f always available as a function
+# instead of defining it empty? otherwise optimized builds of the runtime won't
+# export it but external libraries may pull it in.
+target_compile_options(${_NAME} PRIVATE ${IREE_DEFAULT_COPTS})
+
+target_link_libraries(${_NAME}
+  iree_base_base
+  iree_base_internal_file_io
+  iree_vm_vm
+  iree_vm_bytecode_module
+)
+
+add_subdirectory(test)
diff --git a/samples/custom_module/basic/Makefile b/samples/custom_module/basic/Makefile
new file mode 100644
index 0000000..9a0a630
--- /dev/null
+++ b/samples/custom_module/basic/Makefile
@@ -0,0 +1,51 @@
+# 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 an example showing a basic makefile that links in the IREE runtime by
+# way of the unified static library. It's recommended that IREE is added as a
+# subproject and cmake is used to add the dependencies (as in the CMakeLists.txt
+# in this directory) but when using other build systems this is easier to adapt.
+#
+# Configure the runtime:
+#   cmake -GNinja -B ../iree-build-runtime/ . \
+#       -DCMAKE_BUILD_TYPE=MinSizeRel \
+#       -DIREE_SIZE_OPTIMIZED=ON
+# Build the runtime:
+#   cmake --build ../iree-build-runtime/ --target iree_runtime_unified
+# Make this binary:
+#   make custom-module-run-min RUNTIME_BUILD_DIR=../iree-build-runtime/
+#
+# Note that if IREE_SIZE_OPTIMIZED is used to build the runtime then the
+# -DNDEBUG and -DIREE_STATUS_MODE=0 are required on any binaries using it.
+
+RUNTIME_SRC_DIR ?= ../../runtime/src/
+RUNTIME_BUILD_DIR ?= ../../../iree-build/
+
+SRC_FILES := module.h module.cc main.c
+INCLUDE_DIRS := ${RUNTIME_SRC_DIR}
+INCLUDE_FLAGS := $(addprefix -I,${INCLUDE_DIRS})
+LIBRARY_DIRS := \
+		${RUNTIME_BUILD_DIR}/build_tools/third_party/flatcc/ \
+		${RUNTIME_BUILD_DIR}/runtime/src/iree/runtime/
+LINK_LIBRARIES := \
+    iree_runtime_unified \
+		flatcc_parsing
+LIBRARY_FLAGS := $(addprefix -L,${LIBRARY_DIRS}) $(addprefix -l,${LINK_LIBRARIES})
+CXX_FLAGS := -flto ${INCLUDE_FLAGS} ${LIBRARY_FLAGS}
+MIN_FLAGS := \
+		-s \
+		-Os \
+		-DNDEBUG \
+		-DIREE_STATUS_MODE=0
+
+all: custom-module-run custom-module-run-min
+clean:
+	rm -f custom-module-run custom-module-run-min
+
+custom-module-run: ${SRC_FILES}
+	${CXX} ${SRC_FILES} ${CXX_FLAGS} -o $@
+custom-module-run-min: ${SRC_FILES}
+	${CXX} ${SRC_FILES} ${CXX_FLAGS} ${MIN_FLAGS} -o $@
diff --git a/samples/custom_module/basic/README.md b/samples/custom_module/basic/README.md
new file mode 100644
index 0000000..77d6c28
--- /dev/null
+++ b/samples/custom_module/basic/README.md
@@ -0,0 +1,231 @@
+# Basic custom module sample
+
+This sample shows how to
+
+1. Create a custom module in C++ that can be used with the IREE runtime
+2. Author an MLIR input that uses a custom module including a custom type
+3. Compile that program to an IREE VM bytecode module
+4. Load the compiled program using a low-level VM interface
+5. Call exported functions on the loaded program to exercise the custom module
+
+The custom module is declared in [`module.h`](./module.h), implemented using a
+C++ module wrapper layer in [`module.cc`](./module.cc), and called by example in
+[`main.c`](./main.c).
+
+This document uses terminology that can be found in the documentation of
+[IREE's execution model](https://github.com/iree-org/iree/blob/main/docs/developers/design_docs/execution_model.md).
+See [IREE's extensibility mechanisms](https://iree-org.github.io/iree/extensions/)
+documentation for more information specific to extenting IREE and
+alternative approaches to doing so.
+
+## Background
+
+IREE's VM is used to dynamically link modules of various types together at
+runtime (C, C++, IREE's VM bytecode, etc). Via this mechanism any number of
+modules containing exported functions and types that can be used across modules
+can extend IREE's base functionality. In most IREE programs the HAL module is
+used to provide a hardware abstraction layer for execution and both the HAL
+module itself and the types it exposes (`!hal.buffer`, `!hal.executable`, etc)
+are implemented using this mechanism.
+
+## Instructions
+
+1. Build or install the `iree-compile` binary:
+
+    ```
+    python -m pip install iree-compiler
+    ```
+
+    [See here](https://iree-org.github.io/iree/getting-started/)
+    for general instructions on installing the compiler.
+
+3. Compile the [example module](./test/example.mlir) to a .vmfb file:
+
+    ```
+    # This simple sample doesn't use tensors and can be compiled in host-only
+    # mode to avoid the need for the HAL.
+    iree-compile --iree-execution-model=host-only samples/custom_module/basic/test/example.mlir -o=/tmp/example.vmfb
+    ```
+
+3. Build the `iree_samples_custom_module_run` CMake target :
+
+    ```
+    cmake -B ../iree-build/ -DCMAKE_BUILD_TYPE=RelWithDebInfo . \
+        -DCMAKE_C_FLAGS=-DIREE_VM_EXECUTION_TRACING_FORCE_ENABLE=1
+    cmake --build ../iree-build/ --target iree_samples_custom_module_basic_run
+    ```
+    (here we force runtime execution tracing for demonstration purposes)
+
+    [See here](https://iree-org.github.io/iree/building-from-source/getting-started/)
+    for general instructions on building using CMake.
+
+4. Run the example program to call the main function:
+
+   ```
+   ../iree-build/samples/custom_module/basic/custom-module-basic-run \
+       /tmp/example.vmfb example.main
+   ```
+
+## Defining Custom Modules in C++
+
+Modules are exposed to applications and the IREE VM via the `iree_vm_module_t`
+interface. IREE canonically uses C headers to expose module and type functions
+but the implementation of the module can be anything the user is able to work
+with (C, C++, rust, etc).
+
+A C++ wrapper is provided to ease implementation when minimal code size and overhead is not a focus and provides easy definition of exports and marshaling
+of types. Utilities such as `iree::Status` and `iree::vm::ref<T>` add safety for
+managing reference counted resources and can be used within the modules.
+
+General flow:
+
+1. Expose module via a C API ([`module.h`](./module.h)):
+
+```c
+// Ideally all allocations performed by the module should use |allocator|.
+// The returned module in |out_module| should have a ref count of 1 to transfer
+// ownership to the caller.
+iree_status_t iree_table_module_create(iree_allocator_t allocator,
+                                       iree_vm_module_t** out_module);
+```
+
+2. Implement the module using C/C++/etc ([`module.cc`](./module.cc)):
+
+Modules have two parts: a shared module and instantiated state.
+
+The `iree::vm::NativeModule` helper is used to handle the shared module
+declaration and acts as a factory for per-context instantiated state and the
+methods exported by the module:
+
+```c++
+// Any mutable state stored on the module may be accessed from multiple threads
+// if the module is instantiated in multiple contexts and must be thread-safe.
+struct TableModule final : public vm::NativeModule<TableModuleState> {
+  // Each time the module is instantiated this will be called to allocate the
+  // context-specific state. The returned state must only be thread-compatible
+  // as invocations within a context will not be made from multiple threads but
+  // the thread on which they are made may change over time; this means no TLS!
+  StatusOr<std::unique_ptr<TableModuleState>> CreateState(
+      iree_allocator_t allocator) override;
+};
+```
+
+The module implementation is done on the state object so that methods may use
+`this` to access context-local state:
+
+```c++
+struct TableModuleState final {
+  // Local to the context the module was instantiated in and thread-compatible.
+  std::unordered_map<std::string, std::string> mutable_state;
+
+  // Exported functions must return Status or StatusOr. Failures will result in
+  // program termination and will be propagated up to the top-level invoker.
+  // If a module wants to provide non-fatal errors it can return results to the
+  // program: here we return a 0/1 indicating whether the key was found as well
+  // as the result or null.
+  //
+  // MLIR declaration:
+  //   func.func private @table.lookup(!util.buffer) -> (i1, !util.buffer)
+  StatusOr<std::tuple<int32_t, vm::ref<iree_vm_buffer_t>>> Lookup(
+      const vm::ref<iree_vm_buffer_t> key);
+};
+```
+
+Finally the exported methods are registered and marshaling code is expanded:
+
+```c++
+static const vm::NativeFunction<TableModuleState> kTableModuleFunctions[] = {
+    vm::MakeNativeFunction("lookup", &TableModuleState::Lookup),
+};
+extern "C" iree_status_t iree_table_module_create(
+    iree_allocator_t allocator, iree_vm_module_t** out_module) {
+  auto module = std::make_unique<TableModule>(
+      "table", /*version=*/0, allocator,
+      iree::span<const vm::NativeFunction<CustomModuleState>>
+      (kTableModuleFunctions));
+  *out_module = module.release()->interface();
+  return iree_ok_status();
+}
+```
+
+## Registering Custom Modules at Runtime
+
+Once a custom module is defined it needs to be provided to any context that it
+is going to be used in. Each context may have its own unique mix of modules and
+it's the hosting application's responsibility to inject the available modules.
+See [`main.c`](./main.c) for an example showing the entire end-to-end lifetime
+of loading a compiled bytecode module and providing a custom module for runtime
+dynamic linking.
+
+Since modules themselves can be reused across contexts it can be a way of
+creating shared caches (requires thread-safety!) that span contexts while the
+module state is context specific and isolated.
+
+Import resolution happens in reverse registration order: the most recently
+registered modules override previous ones. This combined with optional imports
+allows overriding behavior and version compatibility shims (though there is
+still some trickiness involved).
+
+```c
+// Ensure custom types are registered before loading modules that use them.
+// This only needs to be done once per instance.
+IREE_CHECK_OK(iree_basic_custom_module_register_types(instance));
+
+// Create the custom module that can be reused across contexts.
+iree_vm_module_t* custom_module = NULL;
+IREE_CHECK_OK(iree_basic_custom_module_create(instance, allocator,
+                                              &custom_module));
+
+// Create the context for this invocation reusing the loaded modules.
+// Contexts hold isolated state and can be reused for multiple calls.
+// Note that the module order matters: the input user module is dependent on
+// the custom module.
+iree_vm_module_t* modules[] = {custom_module, bytecode_module};
+iree_vm_context_t* context = NULL;
+IREE_CHECK_OK(iree_vm_context_create_with_modules(
+    instance, IREE_VM_CONTEXT_FLAG_NONE, IREE_ARRAYSIZE(modules), modules,
+    allocator, &context));
+```
+
+## Calling Custom Modules from Compiled Programs
+
+The IREE compiler allows for external functions that are resolved at runtime
+using the [MLIR `func` dialect](https://mlir.llvm.org/docs/Dialects/Func/). Some
+optional attributes are used to allow for customization where required but in
+many cases no additional IREE-specific work is required in the compiled program.
+A few advanced features of the VM FFI are not currently exposed via this
+mechanism such as variadic arguments and tuples but the advantage is that users
+need not customize the IREE compiler in order to use their modules.
+
+Prior to passing input programs to the IREE compiler users can insert the
+imported functions as external
+[`func.func`](https://mlir.llvm.org/docs/Dialects/Func/#funcfunc-mlirfuncfuncop)
+ops and calls to those functions using
+[`func.call`](https://mlir.llvm.org/docs/Dialects/Func/#funccall-mlirfunccallop):
+
+```mlir
+// An external function declaration.
+// `custom` is the runtime module and `string.create` is the exported method.
+// This call uses both IREE types (`!util.buffer`) and custom ones not known to
+// the compiler but available at runtime (`!custom.string`).
+func.func private @custom.string.create(!util.buffer) -> !custom.string
+```
+
+```mlir
+// Call the imported function.
+%buffer = util.buffer.constant : !util.buffer = "hello world!"
+%result = func.call @custom.string.create(%buffer) : (!util.buffer) -> !custom.string
+```
+
+Users with custom dialects and ops can use
+[MLIR's dialect conversion](https://mlir.llvm.org/docs/DialectConversion/)
+framework to rewrite their custom ops to this form and perform additional
+marshaling logic. For example, the above could have started as this program
+before the user ran their dialect conversion and passed it in to `iree-compile`:
+
+```mlir
+%result = custom.string.create "hello world!" : !custom.string
+```
+
+See this samples [`example.mlir`](./test/example.mlir) for examples of features
+such as signature specification and optional import fallback support.
diff --git a/samples/custom_module/main.c b/samples/custom_module/basic/main.c
similarity index 92%
rename from samples/custom_module/main.c
rename to samples/custom_module/basic/main.c
index bf72aa0..7585033 100644
--- a/samples/custom_module/main.c
+++ b/samples/custom_module/basic/main.c
@@ -29,10 +29,11 @@
 // solar flares, etc).
 int main(int argc, char** argv) {
   if (argc != 3) {
-    fprintf(stderr,
-            "Usage:\n"
-            "  custom-module-run - <entry.point> # read from stdin\n"
-            "  custom-module-run </path/to/say_hello.vmfb> <entry.point>\n");
+    fprintf(
+        stderr,
+        "Usage:\n"
+        "  custom-module-basic-run - <entry.point> # read from stdin\n"
+        "  custom-module-basic-run </path/to/say_hello.vmfb> <entry.point>\n");
     fprintf(stderr, "  (See the README for this sample for details)\n ");
     return -1;
   }
@@ -48,11 +49,12 @@
 
   // Ensure custom types are registered before loading modules that use them.
   // This only needs to be done once.
-  IREE_CHECK_OK(iree_custom_module_register_types(instance));
+  IREE_CHECK_OK(iree_custom_module_basic_register_types(instance));
 
   // Create the custom module that can be reused across contexts.
   iree_vm_module_t* custom_module = NULL;
-  IREE_CHECK_OK(iree_custom_module_create(instance, allocator, &custom_module));
+  IREE_CHECK_OK(
+      iree_custom_module_basic_create(instance, allocator, &custom_module));
 
   // Load the module from stdin or a file on disk.
   // Applications can ship and load modules however they want (such as mapping
diff --git a/samples/custom_module/module.cc b/samples/custom_module/basic/module.cc
similarity index 98%
rename from samples/custom_module/module.cc
rename to samples/custom_module/basic/module.cc
index 36fd997..6ba12d8 100644
--- a/samples/custom_module/module.cc
+++ b/samples/custom_module/basic/module.cc
@@ -67,7 +67,7 @@
   iree_allocator_free(string->allocator, ptr);
 }
 
-extern "C" iree_status_t iree_custom_module_register_types(
+extern "C" iree_status_t iree_custom_module_basic_register_types(
     iree_vm_instance_t* instance) {
   if (iree_custom_string_descriptor.type) {
     return iree_ok_status();  // Already registered.
@@ -184,7 +184,7 @@
 
 // Note that while we are using C++ bindings internally we still expose the
 // module as a C instance. This hides the details of our implementation.
-extern "C" iree_status_t iree_custom_module_create(
+extern "C" iree_status_t iree_custom_module_basic_create(
     iree_vm_instance_t* instance, iree_allocator_t allocator,
     iree_vm_module_t** out_module) {
   IREE_ASSERT_ARGUMENT(out_module);
diff --git a/samples/custom_module/module.h b/samples/custom_module/basic/module.h
similarity index 76%
rename from samples/custom_module/module.h
rename to samples/custom_module/basic/module.h
index 8fdc5c0..c786f6b 100644
--- a/samples/custom_module/module.h
+++ b/samples/custom_module/basic/module.h
@@ -4,8 +4,8 @@
 // See https://llvm.org/LICENSE.txt for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 
-#ifndef IREE_SAMPLES_CUSTOM_MODULE_MODULE_H_
-#define IREE_SAMPLES_CUSTOM_MODULE_MODULE_H_
+#ifndef IREE_SAMPLES_CUSTOM_MODULE_BASIC_MODULE_H_
+#define IREE_SAMPLES_CUSTOM_MODULE_BASIC_MODULE_H_
 
 #include <stdint.h>
 
@@ -31,19 +31,20 @@
                                         iree_custom_string_t** out_string);
 
 // Registers types provided by the custom module.
-iree_status_t iree_custom_module_register_types(iree_vm_instance_t* instance);
+iree_status_t iree_custom_module_basic_register_types(
+    iree_vm_instance_t* instance);
 
 // Creates a native custom module that can be reused in multiple contexts.
 // The module itself may hold state that can be shared by all instantiated
 // copies but it will require the module to provide synchronization; usually
 // it's safer to just treat the module as immutable and keep state within the
 // instantiated module states instead.
-iree_status_t iree_custom_module_create(iree_vm_instance_t* instance,
-                                        iree_allocator_t allocator,
-                                        iree_vm_module_t** out_module);
+iree_status_t iree_custom_module_basic_create(iree_vm_instance_t* instance,
+                                              iree_allocator_t allocator,
+                                              iree_vm_module_t** out_module);
 
 #ifdef __cplusplus
 }  // extern "C"
 #endif  // __cplusplus
 
-#endif  // IREE_SAMPLES_CUSTOM_MODULE_MODULE_H_
+#endif  // IREE_SAMPLES_CUSTOM_MODULE_BASIC_MODULE_H_
diff --git a/samples/custom_module/test/CMakeLists.txt b/samples/custom_module/basic/test/CMakeLists.txt
similarity index 89%
rename from samples/custom_module/test/CMakeLists.txt
rename to samples/custom_module/basic/test/CMakeLists.txt
index a6e86ce..ad7361a 100644
--- a/samples/custom_module/test/CMakeLists.txt
+++ b/samples/custom_module/basic/test/CMakeLists.txt
@@ -12,7 +12,7 @@
   TOOLS
     FileCheck
     iree-compile
-    iree_samples_custom_module_run
+    iree_samples_custom_module_basic_run
   LABELS
     "hostonly"
 )
diff --git a/samples/custom_module/test/example.mlir b/samples/custom_module/basic/test/example.mlir
similarity index 98%
rename from samples/custom_module/test/example.mlir
rename to samples/custom_module/basic/test/example.mlir
index 07aa11f..c3a4a26 100644
--- a/samples/custom_module/test/example.mlir
+++ b/samples/custom_module/basic/test/example.mlir
@@ -1,4 +1,4 @@
-// RUN: iree-compile %s --iree-execution-model=host-only | custom-module-run - example.main | FileCheck %s
+// RUN: iree-compile %s --iree-execution-model=host-only | custom-module-basic-run - example.main | FileCheck %s
 
 module @example {
   //===--------------------------------------------------------------------===//