# 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://iree.dev/developers/design-docs/invocation-execution-model/).
See [IREE's extensibility mechanisms](https://iree.dev/reference/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-base-compiler` binary:

    ```
    python -m pip install iree-base-compiler
    ```

    [See here](https://iree.dev/reference/bindings/python/)
    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/ -G Ninja -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.dev/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.
