blob: 3db1f1419804e156bce4480b41b3e0816c1d35f3 [file] [log] [blame]
// Copyright 2023 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 <cstdio>
#include "iree/base/api.h"
#include "iree/hal/api.h"
#include "iree/modules/hal/types.h"
#include "iree/vm/api.h"
#include "iree/vm/dynamic/api.h"
#include "iree/vm/native_module_cc.h"
// NOTE: this module is written in C++ using the native module wrapper and uses
// template magic to handle marshaling arguments. For a lot of uses this is a
// much friendlier way of exposing modules to the IREE VM and if performance and
// code size are not a concern is a fine route to take. Here we do it for
// brevity but all of the internal IREE modules are implemented in C.
//===----------------------------------------------------------------------===//
// !custom.string type
//===----------------------------------------------------------------------===//
// The "string" type we use to store and retain string data.
// This could be arbitrarily complex or simply wrap another user-defined type.
// The descriptor that is registered at startup defines how to manage the
// lifetime of the type (such as which destruction function is called, if any).
// See ref.h for more information and additional utilities.
typedef struct iree_custom_string_t {
// Must be the first field; used to track the reference count of the object.
iree_vm_ref_object_t ref_object;
// Allocator the string data was allocated from.
// Ideally pools and nested allocators would be used to avoid needing to store
// the allocator with every object.
iree_allocator_t allocator;
// Non-NUL-terminated string value.
iree_string_view_t value;
} iree_custom_string_t;
// Runtime type descriptor for the !custom.string describing how to manage it
// and destroy it. The type ID is allocated at runtime and does not need to
// match the compiler ID.
IREE_VM_DECLARE_TYPE_ADAPTERS(iree_custom_string, iree_custom_string_t);
IREE_VM_DEFINE_TYPE_ADAPTERS(iree_custom_string, iree_custom_string_t);
// Creates a new !custom.string object with a copy of the given |value|.
// Applications could use this and any other methods we wanted to expose to
// interop with the loaded VM modules - such as passing in/out the objects.
// We don't need this for the demo but creating the custom object, appending it
// to the invocation input list, and then consuming it in the compiled module
// is straightforward.
static iree_status_t iree_custom_string_create(
iree_string_view_t value, iree_allocator_t allocator,
iree_custom_string_t** out_string) {
IREE_ASSERT_ARGUMENT(out_string);
// Note that we allocate the string and the string value together.
iree_custom_string_t* string = NULL;
IREE_RETURN_IF_ERROR(iree_allocator_malloc(
allocator, sizeof(*string) + value.size, (void**)&string));
string->ref_object.counter = IREE_ATOMIC_VAR_INIT(1);
string->allocator = allocator;
string->value.data = ((const char*)string) + sizeof(iree_custom_string_t);
string->value.size = value.size;
memcpy((void*)string->value.data, value.data, string->value.size);
*out_string = string;
return iree_ok_status();
}
static void iree_custom_string_destroy(void* ptr) {
iree_custom_string_t* string = (iree_custom_string_t*)ptr;
iree_allocator_free(string->allocator, ptr);
}
static iree_vm_ref_type_descriptor_t iree_custom_string_descriptor_storage = {
0};
// Registers types provided by the custom module.
// We must call this before any of our types can be resolved.
static iree_status_t iree_custom_module_basic_register_types(
iree_vm_instance_t* instance) {
iree_custom_string_descriptor_storage.destroy = iree_custom_string_destroy;
iree_custom_string_descriptor_storage.type_name = IREE_SV("custom.string");
iree_custom_string_descriptor_storage.offsetof_counter =
offsetof(iree_custom_string_t, ref_object.counter) /
IREE_VM_REF_COUNTER_ALIGNMENT;
return iree_vm_instance_register_type(instance,
&iree_custom_string_descriptor_storage,
&iree_custom_string_registration);
}
// Unregisters types previously registered.
// In dynamic modules it's critical that types are unregistered before the
// library is unloaded.
static void iree_custom_module_basic_unregister_types(
iree_vm_instance_t* instance) {
iree_vm_instance_unregister_type(instance,
&iree_custom_string_descriptor_storage);
}
//===----------------------------------------------------------------------===//
// VM module interface implementation
//===----------------------------------------------------------------------===//
namespace {
using namespace iree;
// Per-context module state.
// This can contain "globals" and other arbitrary state.
//
// Thread-compatible; the runtime will not issue multiple calls at the same
// time using the same state. If the implementation uses external threads then
// it must synchronize itself.
class CustomModuleState final {
public:
explicit CustomModuleState(iree_allocator_t host_allocator)
: host_allocator_(host_allocator) {}
~CustomModuleState() = default;
// Creates a new string with a copy of the given string data.
// No NUL terminator is required.
StatusOr<vm::ref<iree_custom_string_t>> StringFromTensor(
vm::ref<iree_hal_buffer_view_t> buffer_view) {
char string_buffer[512];
iree_host_size_t string_length = 0;
IREE_RETURN_IF_ERROR(iree_hal_buffer_view_format(
buffer_view.get(), 128, IREE_ARRAYSIZE(string_buffer), string_buffer,
&string_length));
vm::ref<iree_custom_string_t> string;
IREE_RETURN_IF_ERROR(iree_custom_string_create(
iree_make_string_view(string_buffer, string_length), host_allocator_,
&string));
fprintf(stdout, "CREATE %.*s\n", static_cast<int>(string->value.size),
string->value.data);
fflush(stdout);
return std::move(string);
}
// Prints the contents of the string to stdout.
Status StringPrint(const vm::ref<iree_custom_string_t> string) {
if (!string) return OkStatus(); // no-op
fprintf(stdout, "PRINT %.*s\n", static_cast<int>(string->value.size),
string->value.data);
fflush(stdout);
return OkStatus();
}
// Prints the contents of the string to stdout.
Status ThrowError() {
return iree_make_status(IREE_STATUS_UNKNOWN, "Forced failure");
}
private:
// Allocator that the caller requested we use for any allocations we need to
// perform during operation.
iree_allocator_t host_allocator_;
};
// Function table mapping imported function names to their implementation.
static const vm::NativeFunction<CustomModuleState> kCustomModuleFunctions[] = {
vm::MakeNativeFunction("string.from_tensor",
&CustomModuleState::StringFromTensor),
vm::MakeNativeFunction("string.print", &CustomModuleState::StringPrint),
vm::MakeNativeFunction("error", &CustomModuleState::ThrowError),
};
// The module instance that will be allocated and reused across contexts.
// Any context-specific state must be stored in a state structure such as
// CustomModuleState.
//
// Assumed thread-safe (by construction here, as it's immutable), though if any
// mutable state is stored here it will need to be synchronized by the
// implementation.
class CustomModule final : public vm::NativeModule<CustomModuleState> {
public:
using vm::NativeModule<CustomModuleState>::NativeModule;
~CustomModule() override {
iree_custom_module_basic_unregister_types(instance());
}
// Creates per-context state when the module is added to a new context.
// May be called from any thread.
StatusOr<std::unique_ptr<CustomModuleState>> CreateState(
iree_allocator_t host_allocator) override {
auto state = std::make_unique<CustomModuleState>(host_allocator);
return state;
}
// Forks a parent state into a child state, preserving any module state
// by-reference.
StatusOr<std::unique_ptr<CustomModuleState>> ForkState(
CustomModuleState* parent_state,
iree_allocator_t host_allocator) override {
// No special state to preserve.
return CreateState(host_allocator);
}
};
} // namespace
// 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.
//
// 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 and
// is required for working across the dynamic library boundary.
extern "C" IREE_VM_DYNAMIC_MODULE_EXPORT iree_status_t create_custom_module(
iree_vm_dynamic_module_version_t max_version, iree_vm_instance_t* instance,
iree_host_size_t param_count, const iree_string_pair_t* params,
iree_allocator_t host_allocator, iree_vm_module_t** out_module) {
// Ensure the version matches; the version will change if the VM module
// interface changes and existing libraries are incompatible.
if (max_version != IREE_VM_DYNAMIC_MODULE_VERSION_LATEST) {
return iree_make_status(
IREE_STATUS_UNIMPLEMENTED,
"unsupported runtime version %u, module compiled with version %u",
max_version, IREE_VM_DYNAMIC_MODULE_VERSION_LATEST);
}
#if IREE_TRACING_FEATURES
// Today Tracy cannot be used with custom dynamic modules as it'll try to
// create a new tracing context distinct from the hosting application. Custom
// module libraries should be built with tracing disabled.
fprintf(stderr,
"Tracy is not currently supported in custom dynamic modules\n");
#endif // IREE_TRACING_FEATURES
// Ensure HAL types are available. We need to do this as we're being
// dynamically loaded and can't automatically access the hosting process
// variables.
IREE_RETURN_IF_ERROR(iree_hal_module_resolve_all_types(instance));
// Register custom types used by the module against the instance.
// Note that this function must be safe to call multiple times as the module
// may be loaded multiple times.
IREE_RETURN_IF_ERROR(iree_custom_module_basic_register_types(instance));
// Create the custom module and return it to the runtime.
// NOTE: this isn't using the allocator here and that's bad as it leaves
// untracked allocations and pulls in the system allocator that may differ
// from the one requested by the user.
// TODO(benvanik): std::allocator wrapper around iree_allocator_t so this can
// use that instead.
auto module = std::make_unique<CustomModule>(
"custom", /*version=*/0, instance, host_allocator,
iree::span<const vm::NativeFunction<CustomModuleState>>(
kCustomModuleFunctions));
*out_module = module.release()->interface();
return iree_ok_status();
}