Repository: mun-lang/mun (language: rust)

Issue:
Cyclic type reference causes hang
A runtime bug was introduced when loading an assembly that contains a cyclic datastructure. The following code is valid Mun code because `Foo` is garbage collected type (even though its impossible to create this type at runtime).

```rust
pub struct Foo {
    foo: Foo
}
```

When loading a munlib at runtime that contains this type the runtime hangs. This is due to the code in assembly.rs:

https://github.com/mun-lang/mun/blob/fa07c5c9ae85b27cff5faf7e873583bf10e2de86/crates/mun_runtime/src/assembly.rs#L215-L223

When loading the type `Foo`, if the type `Foo` is not yet in the `TypeTable` loading the above snippet will fail.

We could do something by first creating the `Arc<TypeInfo>` and then injecting it somehow but it raises another interesting question. By storing the `Arc<TypeInfo>` in the field, we create a cycle. However, storing the type of a field as a `Weak<TypeInfo>` is also not an option in a case like this:

```rust
pub struct Foo {
    bar: Bar
}

pub struct Bar {
    foo: Foo
}
```

If in the case above we only hold an `Arc<TypeInfo>` of `Bar`, the type info of `Foo` might be dropped which is not what we want.

I see two approaches in solving this:

* Always retain all the TypeInfos ever allocated. Memory will grow over time, but I doubt it will become a major issue.
* Garbage collect TypeInfos. 

The first option is definitely the easiest to implement, I recommend we first take that approach and optimize later. WDYT?


Discussion / hints:
Here is a `mun_hir` test that hangs currently:

```rust
#[test]
fn cyclic_struct() {
    let driver = CompileAndRunTestDriver::new(
        r"
        pub struct Foo {
            foo: Foo
        }
        ",
        |builder| builder,
    );
    driver.unwrap();
}
```

Fix patch:
diff --git a/cpp/include/mun/error.h b/cpp/include/mun/error.h
index 3d683cd81..1d551ba02 100644
--- a/cpp/include/mun/error.h
+++ b/cpp/include/mun/error.h
@@ -1,6 +1,9 @@
 #ifndef MUN_ERROR_H_
 #define MUN_ERROR_H_
 
+#include <optional>
+#include <string_view>
+
 #include "mun/runtime_capi.h"
 
 namespace mun {
@@ -11,45 +14,72 @@ namespace mun {
 class Error {
 public:
     /** Default constructs an error. */
-    Error() noexcept : m_handle{nullptr} {}
+    constexpr Error() noexcept : m_handle{nullptr} {}
 
     /** Constructs an error from a `MunErrorHandle`.
      *
-     * \param handle an error handle
+     * \param handle an error type_handle
      */
-    Error(MunErrorHandle handle) noexcept : m_handle(handle) {}
+    constexpr Error(MunErrorHandle handle) noexcept : m_handle(handle) {}
 
     /** Move constructs an error.
      *
      * \param other an rvalue reference to an error
      */
-    Error(Error&& other) noexcept : m_handle(other.m_handle) { other.m_handle._0 = nullptr; }
+    constexpr Error(Error&& other) noexcept : m_handle(other.m_handle) {
+        other.m_handle.error_string = nullptr;
+    }
 
     /** Move assigns an error.
      *
      * \param other an rvalue reference to an error
      */
-    Error& operator=(Error&& other) noexcept {
+    constexpr Error& operator=(Error&& other) noexcept {
         m_handle = other.m_handle;
-        other.m_handle._0 = nullptr;
+        other.m_handle.error_string = nullptr;
         return *this;
     }
 
     /** Destructs the error. */
     ~Error() noexcept { mun_error_destroy(m_handle); }
 
-    /** Returns the error message, if it exists, otherwise returns a nullptr.
-     *
-     * The message is UTF-8 encoded.
+    /**
+     * Returns the error message, if it exists, otherwise returns `std::nullopt`. The message is
+     * UTF-8 encoded.
+     */
+    [[nodiscard]] constexpr std::optional<std::string_view> message() const noexcept {
+        if (m_handle.error_string != nullptr) return std::make_optional(m_handle.error_string);
+        return std::nullopt;
+    }
+
+    /**
+     * Returns true if this instance contains an error message. False if the result is Ok.
      */
-    const char* message() noexcept { return m_handle._0; }
+    [[nodiscard]] constexpr inline bool is_error() const {
+        return m_handle.error_string != nullptr;
+    }
 
-    /** Retrieves whether an error occurred */
-    operator bool() const noexcept { return m_handle._0 != nullptr; }
+    /**
+     * Returns true if this instance represents an OK result.
+     */
+    [[nodiscard]] constexpr inline bool is_ok() const { return m_handle.error_string == nullptr; }
 
 private:
     MunErrorHandle m_handle;
 };
 }  // namespace mun
 
+#ifdef NDEBUG
+#define MUN_ASSERT(expr) ((void)expr)
+#else
+#define MUN_ASSERT(expr)                                                           \
+    do {                                                                           \
+        ::mun::Error err(expr);                                                    \
+        if (auto message = err.message(); message) {                               \
+            std::cerr << "Assertion Error: " #expr " failed: " << message.value(); \
+            std::abort();                                                          \
+        }                                                                          \
+    } while (0)
+#endif
+
 #endif
diff --git a/cpp/include/mun/field_info.h b/cpp/include/mun/field_info.h
index d8e67cc7e..e9a071a8c 100644
--- a/cpp/include/mun/field_info.h
+++ b/cpp/include/mun/field_info.h
@@ -4,88 +4,148 @@
 #include <cassert>
 
 #include "mun/runtime_capi.h"
-#include "mun/type_info.h"
+#include "mun/type.h"
 
 namespace mun {
 /**
- * @brief A wrapper around a Mun field information handle.
+ * @brief A wrapper around a MunField.
+ *
+ * The Type from which this Field came must be kept alive during the lifetime of this instance.
  */
 class FieldInfo {
 public:
     /**
      * @brief Constructs field information from an instantiated `MunFieldInfoHandle`.
      */
-    FieldInfo(MunFieldInfoHandle handle) noexcept : m_handle(handle) {}
+    constexpr explicit FieldInfo(MunField handle) noexcept : m_handle(handle) {}
 
     /**
      * @brief Retrieves the field's name.
      */
-    std::string_view name() const noexcept {
-        const auto ptr = mun_field_info_name(m_handle);
-        assert(ptr);
-        return ptr;
+    [[nodiscard]] std::string name() const noexcept {
+        const char* name;
+        MUN_ASSERT(mun_field_name(m_handle, &name));
+        std::string str(name);
+        mun_string_destroy(name);
+        return str;
     }
 
     /**
      * @brief Retrieves the field's type.
      */
-    TypeInfo type() const noexcept {
-        const auto type_handle = mun_field_info_type(m_handle);
-        assert(type_handle._0);
-        return TypeInfo(type_handle);
+    [[nodiscard]] Type type() const noexcept {
+        MunType ty;
+        MUN_ASSERT(mun_field_type(m_handle, &ty));
+        return Type(ty);
     }
 
     /**
      * @brief Retrieves the field's offset.
      */
-    uint16_t offset() const noexcept {
-        uint16_t offset;
-        const auto error_handle = mun_field_info_offset(m_handle, &offset);
-        assert(error_handle._0 == nullptr);
+    [[nodiscard]] uintptr_t offset() const noexcept {
+        uintptr_t offset;
+        MUN_ASSERT(mun_field_offset(m_handle, &offset));
         return offset;
     }
 
 private:
-    MunFieldInfoHandle m_handle;
+    MunField m_handle;
 };
 
 /**
- * @brief A wrapper around a span of Mun field informations, which are owned by the Mun runtime.
+ * @brief A wrapper around MunTypes. Stores field information of a struct.
+ *
+ * Note that the StructType must not go out of scope or undefined behavior can occur.
  */
-class FieldInfoSpan {
+class StructFields {
 public:
+    struct Iterator {
+        using iterator_category = std::random_access_iterator_tag;
+        using difference_type = std::ptrdiff_t;
+        using value_type = FieldInfo;
+
+        constexpr explicit Iterator(MunField const* ptr) : m_ptr(ptr){};
+
+        value_type operator*() const { return value_type(*m_ptr); }
+
+        Iterator& operator++() {
+            m_ptr++;
+            return *this;
+        }
+
+        Iterator operator++(int) {
+            Iterator tmp = *this;
+            ++(*this);
+            return tmp;
+        }
+
+        friend bool operator==(const Iterator& a, const Iterator& b) { return a.m_ptr == b.m_ptr; };
+        friend bool operator!=(const Iterator& a, const Iterator& b) { return a.m_ptr != b.m_ptr; };
+
+    private:
+        MunField const* m_ptr;
+    };
+
     /**
-     * @brief Constructs a field information span from an instantiated `MunFieldInfoSpan`.
+     * @brief Constructs a field information span from an instantiated `MunTypes`.
+     *
+     * This function assumes ownership is transferred.
      */
-    FieldInfoSpan(MunFieldInfoSpan span) noexcept : m_span(span) {}
+    constexpr explicit StructFields(MunFields fields) noexcept : m_data(fields) {}
 
-    FieldInfoSpan(FieldInfoSpan&&) = default;
-    FieldInfoSpan(const FieldInfoSpan&) = delete;
+    constexpr StructFields(StructFields&& other) noexcept : m_data(other.m_data) {
+        other.m_data.fields = nullptr;
+    }
+    StructFields(const StructFields&) = delete;
+
+    ~StructFields() noexcept {
+        if (m_data.fields != nullptr) {
+            mun_fields_destroy(m_data);
+            m_data.fields = nullptr;
+        }
+    }
 
-    ~FieldInfoSpan() noexcept { mun_field_info_span_destroy(m_span); }
+    StructFields& operator=(const StructFields&) = delete;
+    StructFields& operator=(StructFields&& other) noexcept {
+        std::swap(m_data, other.m_data);
+        return *this;
+    }
 
     /**
      * @brief Returns an iterator to the beginning.
      */
-    const MunFieldInfoHandle* begin() const noexcept { return data(); }
+    [[nodiscard]] inline constexpr Iterator begin() const noexcept {
+        return Iterator(m_data.fields);
+    }
 
     /**
      * @brief Returns an iterator to the end.
      */
-    const MunFieldInfoHandle* end() const noexcept { return data() + size() + 1; }
+    [[nodiscard]] inline constexpr Iterator end() const noexcept {
+        return Iterator(m_data.fields + m_data.count);
+    }
 
     /**
-     * @brief Returns a pointer to the beginning of the sequence of elements.
+     * Finds a certain field by its name.
      */
-    const MunFieldInfoHandle* data() const noexcept { return m_span.data; }
+    [[nodiscard]] std::optional<FieldInfo> find_by_name(std::string_view name) const {
+        MunField field;
+        bool has_field;
+        MUN_ASSERT(mun_fields_find_by_name(m_data, name.data(), name.size(), &has_field, &field));
+        if (has_field) {
+            return std::make_optional(FieldInfo(field));
+        } else {
+            return std::nullopt;
+        }
+    }
 
     /**
-     * @brief Returns the number of elements in the sequence.
+     * @brief Returns the number of fields.
      */
-    size_t size() const noexcept { return m_span.len; }
+    [[nodiscard]] inline size_t size() const noexcept { return m_data.count; }
 
 private:
-    MunFieldInfoSpan m_span;
+    MunFields m_data;
 };
 }  // namespace mun
 
diff --git a/cpp/include/mun/function.h b/cpp/include/mun/function.h
new file mode 100644
index 000000000..d7c47ec93
--- /dev/null
+++ b/cpp/include/mun/function.h
@@ -0,0 +1,87 @@
+#ifndef MUN_FUNCTION_INFO_H_
+#define MUN_FUNCTION_INFO_H_
+
+#include <cassert>
+
+#include "mun/runtime_capi.h"
+#include "mun/type.h"
+
+namespace mun {
+/**
+ * @brief A wrapper around a Mun function handle.
+ */
+class Function {
+public:
+    /**
+     * @brief Constructs type information from a MunFunction handle.
+     *
+     * This function assumes that ownership is transferred.
+     */
+    constexpr explicit Function(MunFunction handle) noexcept : m_handle(handle) {}
+
+    Function(const Function& other) : m_handle(other.m_handle) {
+        MUN_ASSERT(mun_function_add_reference(m_handle));
+    }
+    constexpr Function(Function&& other) : m_handle(other.m_handle) { other.m_handle._0 = nullptr; }
+
+    ~Function() {
+        if (m_handle._0 != nullptr) {
+            MUN_ASSERT(mun_function_release(m_handle));
+            m_handle._0 = nullptr;
+        }
+    }
+
+    Function& operator=(const Function& other) {
+        if (other.m_handle._0 != nullptr) {
+            MUN_ASSERT(mun_function_add_reference(other.m_handle));
+        }
+        if (m_handle._0 != nullptr) {
+            MUN_ASSERT(mun_function_release(m_handle));
+        }
+        m_handle = other.m_handle;
+    }
+
+    /**
+     * @brief Retrieves the function's name.
+     */
+    [[nodiscard]] std::string name() const noexcept {
+        const char* name;
+        MUN_ASSERT(mun_function_name(m_handle, &name));
+        std::string name_str(name);
+        mun_string_destroy(name);
+        return name_str;
+    }
+
+    /**
+     * @brief Retrieves the function's argument types.
+     */
+    [[nodiscard]] TypeArray argument_types() const noexcept {
+        MunTypes types;
+        MUN_ASSERT(mun_function_argument_types(m_handle, &types));
+        return TypeArray(types);
+    }
+
+    /**
+     * @brief Retrieves the function's return type.
+     */
+    [[nodiscard]] Type return_type() const noexcept {
+        MunType handle;
+        MUN_ASSERT(mun_function_return_type(m_handle, &handle));
+        return Type(handle);
+    }
+
+    /**
+     * @brief Retrieves the function's pointer.
+     */
+    [[nodiscard]] const void* function_pointer() const noexcept {
+        const void* ptr;
+        MUN_ASSERT(mun_function_fn_ptr(m_handle, &ptr));
+        return ptr;
+    }
+
+private:
+    MunFunction m_handle;
+};
+}  // namespace mun
+
+#endif  // MUN_FUNCTION_INFO_H_
diff --git a/cpp/include/mun/function_info.h b/cpp/include/mun/function_info.h
deleted file mode 100644
index 20a9282a9..000000000
--- a/cpp/include/mun/function_info.h
+++ /dev/null
@@ -1,62 +0,0 @@
-#ifndef MUN_FUNCTION_INFO_H_
-#define MUN_FUNCTION_INFO_H_
-
-#include <cassert>
-
-#include "mun/runtime_capi.h"
-#include "mun/type_info.h"
-
-namespace mun {
-/**
- * @brief A wrapper around a Mun function information handle.
- */
-class FunctionInfo {
-public:
-    /**
-     * @brief Constructs type information from an instantiated `MunFunctionInfoHandle`.
-     */
-    FunctionInfo(MunFunctionInfoHandle handle) noexcept : m_handle(handle) {}
-
-    /**
-     * @brief Retrieves the function's name.
-     */
-    std::string_view name() const noexcept {
-        const auto ptr = mun_function_info_name(m_handle);
-        assert(ptr);
-        return ptr;
-    }
-
-    /**
-     * @brief Retrieves the function's argument types.
-     */
-    TypeInfoSpan argument_types() const noexcept {
-        MunTypeInfoSpan span;
-        const auto error_handle = mun_function_info_argument_types(m_handle, &span);
-        assert(error_handle._0 == nullptr);
-        return TypeInfoSpan(span);
-    }
-
-    /**
-     * @brief Retrieves the function's return type.
-     */
-    TypeInfo return_type() const noexcept {
-        const auto handle = mun_function_info_return_type(m_handle);
-        assert(handle._0);
-        return TypeInfo(handle);
-    }
-
-    /**
-     * @brief Retrieves the function's pointer.
-     */
-    const void* function_pointer() const noexcept {
-        const void* fn_ptr = mun_function_info_fn_ptr(m_handle);
-        assert(fn_ptr);
-        return fn_ptr;
-    }
-
-private:
-    MunFunctionInfoHandle m_handle;
-};
-}  // namespace mun
-
-#endif  // MUN_FUNCTION_INFO_H_
diff --git a/cpp/include/mun/gc.h b/cpp/include/mun/gc.h
index 9cfc27b4f..30d092c9f 100644
--- a/cpp/include/mun/gc.h
+++ b/cpp/include/mun/gc.h
@@ -11,10 +11,10 @@ class Runtime;
 class GcRootPtr {
 public:
     /** Constructs a rooted garbage collection pointer from the provided raw
-     * garbage collection handle.
+     * garbage collection type_handle.
      *
      * \param runtime a reference to a runtime
-     * \param obj a garbage collected object handle
+     * \param obj a garbage collected object type_handle
      * \return a rooted garbage collection pointer
     .*/
     GcRootPtr(const Runtime& runtime, MunGcPtr obj) noexcept : m_ptr(obj), m_runtime(&runtime) {
@@ -59,16 +59,16 @@ class GcRootPtr {
     /** Destructs the `GcRootPtr`, unrooting the underlying `GcPtr`. */
     ~GcRootPtr() noexcept { unroot(); }
 
-    /** Retrieves the raw garbage collection handle of this instance.
+    /** Retrieves the raw garbage collection type_handle of this instance.
      *
-     * \return a raw garbage collection handle
+     * \return a raw garbage collection type_handle
      */
-    MunGcPtr handle() const noexcept { return m_ptr; }
+    [[nodiscard]] constexpr MunGcPtr handle() const noexcept { return m_ptr; }
 
     /** Unroots the underlying `GcPtr`, returning the underlying garbage
-     * collection handle.
+     * collection type_handle.
      *
-     * \return a raw garbage collection handle
+     * \return a raw garbage collection type_handle
      */
     MunGcPtr unroot() noexcept {
         const auto ptr = m_ptr;
diff --git a/cpp/include/mun/invoke_fn.h b/cpp/include/mun/invoke_fn.h
index 894f05e36..9cb0c473c 100644
--- a/cpp/include/mun/invoke_fn.h
+++ b/cpp/include/mun/invoke_fn.h
@@ -33,8 +33,8 @@ InvokeResult<Output, Args...> invoke_fn(Runtime& runtime, std::string_view fn_na
 
     Error error;
     constexpr auto NUM_ARGS = sizeof...(Args);
-    if (auto fn_info = runtime.find_function_info(fn_name, &error); error) {
-        std::cerr << "Failed to retrieve function info due to error: " << error.message()
+    if (auto fn_info = runtime.find_function_info(fn_name, &error); error.is_error()) {
+        std::cerr << "Failed to retrieve function info due to error: " << error.message().value()
                   << std::endl;
     } else if (!fn_info) {
         std::cerr << "Failed to obtain function '" << fn_name << "'" << std::endl;
@@ -49,9 +49,9 @@ InvokeResult<Output, Args...> invoke_fn(Runtime& runtime, std::string_view fn_na
         }
 
         if constexpr (NUM_ARGS > 0) {
-            const MunTypeInfoHandle* arg_ptr = arg_types.begin();
+            auto arg_it = arg_types.begin();
             const std::optional<std::pair<std::string, std::string>> return_type_diffs[] = {
-                reflection::equals_argument_type(TypeInfo(*(arg_ptr++)), args)...};
+                reflection::equals_argument_type(*(arg_it++), args)...};
 
             for (size_t idx = 0; idx < NUM_ARGS; ++idx) {
                 if (auto diff = return_type_diffs[idx]) {
diff --git a/cpp/include/mun/marshal.h b/cpp/include/mun/marshal.h
index cf67db381..fcd8a4df1 100644
--- a/cpp/include/mun/marshal.h
+++ b/cpp/include/mun/marshal.h
@@ -7,27 +7,27 @@ namespace mun {
 template <typename T>
 struct Marshal;
 
-#define IMPL_PRIMITIVE_TYPE_MARSHAL(ty)                                                            \
-    template <>                                                                                    \
-    struct Marshal<ty> {                                                                           \
-        using type = ty;                                                                           \
-                                                                                                   \
-        static type from(type value, const Runtime&) noexcept { return value; }                    \
-                                                                                                   \
-        static type to(type value) noexcept { return value; }                                      \
-                                                                                                   \
-        static type copy_from(const type* value, const Runtime&, const TypeInfo&) noexcept {       \
-            return *value;                                                                         \
-        }                                                                                          \
-                                                                                                   \
-        static void move_to(type value, type* ptr, const TypeInfo&) noexcept {                     \
-            *ptr = std::move(value);                                                               \
-        }                                                                                          \
-                                                                                                   \
-        static type swap_at(type value, type* ptr, const Runtime&, const TypeInfo&) noexcept {     \
-            std::swap(value, *ptr);                                                                \
-            return std::move(value);                                                               \
-        }                                                                                          \
+#define IMPL_PRIMITIVE_TYPE_MARSHAL(ty)                                                    \
+    template <>                                                                            \
+    struct Marshal<ty> {                                                                   \
+        using type = ty;                                                                   \
+                                                                                           \
+        static type from(type value, const Runtime&) noexcept { return value; }            \
+                                                                                           \
+        static type to(type value) noexcept { return value; }                              \
+                                                                                           \
+        static type copy_from(const type* value, const Runtime&, const Type&) noexcept {   \
+            return *value;                                                                 \
+        }                                                                                  \
+                                                                                           \
+        static void move_to(type value, type* ptr, const Type&) noexcept {                 \
+            *ptr = std::move(value);                                                       \
+        }                                                                                  \
+                                                                                           \
+        static type swap_at(type value, type* ptr, const Runtime&, const Type&) noexcept { \
+            std::swap(value, *ptr);                                                        \
+            return std::move(value);                                                       \
+        }                                                                                  \
     };
 
 // TODO: Add support for 128-bit integers
diff --git a/cpp/include/mun/reflection.h b/cpp/include/mun/reflection.h
index 8416b9813..4f977d5cc 100644
--- a/cpp/include/mun/reflection.h
+++ b/cpp/include/mun/reflection.h
@@ -7,27 +7,36 @@
 #include <optional>
 
 #include "mun/runtime_capi.h"
-#include "mun/type_info.h"
+#include "mun/type.h"
 
 namespace mun {
 
 template <typename T>
 struct ArgumentReflection {
-    static TypeInfo type_info(const T&) noexcept { return StaticTypeInfo<T>::type_info(); }
+    static Type type_info(const T&) noexcept { return StaticTypeInfo<T>::type_info(); }
 };
 
 template <typename T>
 struct ReturnTypeReflection {
-    static bool accepts_type(const TypeInfo& ty) noexcept {
+    static bool accepts_type(const Type& ty) noexcept {
         return StaticTypeInfo<T>::type_info() == ty;
     }
-    static std::string_view type_hint() { return StaticTypeInfo<T>::type_info().name(); }
+    static std::string type_hint() { return StaticTypeInfo<T>::type_info().name(); }
+};
+
+template <>
+struct ReturnTypeReflection<void> {
+    static bool accepts_type(const Type& ty) noexcept {
+        return StaticTypeInfo<void>::type_info() == ty ||
+               StaticTypeInfo<std::tuple<>>::type_info() == ty;
+    }
+    static std::string type_hint() { return StaticTypeInfo<void>::type_info().name(); }
 };
 
 namespace reflection {
 template <typename Arg>
 inline std::optional<std::pair<std::string, std::string>> equals_argument_type(
-    const TypeInfo& type_info, const Arg& arg) noexcept {
+    const Type& type_info, const Arg& arg) noexcept {
     auto arg_type_info = ArgumentReflection<Arg>::type_info(arg);
     if (arg_type_info == type_info) {
         return std::nullopt;
@@ -38,7 +47,7 @@ inline std::optional<std::pair<std::string, std::string>> equals_argument_type(
 }
 template <typename T>
 inline std::optional<std::pair<std::string, std::string>> equals_return_type(
-    const TypeInfo& type_info) noexcept {
+    const Type& type_info) noexcept {
     if (!ReturnTypeReflection<T>::accepts_type(type_info)) {
         return std::make_pair(std::string(type_info.name()),
                               std::string(ReturnTypeReflection<T>::type_hint()));
diff --git a/cpp/include/mun/runtime.h b/cpp/include/mun/runtime.h
index d41754564..000ba85f8 100644
--- a/cpp/include/mun/runtime.h
+++ b/cpp/include/mun/runtime.h
@@ -6,10 +6,10 @@
 #include <string_view>
 
 #include "mun/error.h"
-#include "mun/function_info.h"
+#include "mun/function.h"
 #include "mun/runtime_capi.h"
 #include "mun/runtime_function.h"
-#include "mun/type_info.h"
+#include "mun/type.h"
 
 namespace mun {
 
@@ -26,9 +26,9 @@ class Runtime {
 
     /** Constructs a runtime from an instantiated `MunRuntimeHandle`.
      *
-     * \param handle a runtime handle
+     * \param handle a runtime type_handle
      */
-    Runtime(MunRuntimeHandle handle) noexcept : m_handle(handle) {}
+    Runtime(MunRuntime handle) noexcept : m_handle(handle) {}
 
 public:
     /** Move constructs a runtime
@@ -47,19 +47,20 @@ class Runtime {
      * \param out_error a pointer that will optionally return an error
      * \return possibly, the desired `MunFunctionDefinition` struct
      */
-    std::optional<FunctionInfo> find_function_info(std::string_view fn_name,
-                                                   Error* out_error = nullptr) noexcept {
+    std::optional<Function> find_function_info(std::string_view fn_name,
+                                               Error* out_error = nullptr) noexcept {
         bool has_fn;
-        MunFunctionInfoHandle fn_info;
-        if (auto error =
-                Error(mun_runtime_get_function_info(m_handle, fn_name.data(), &has_fn, &fn_info))) {
+        MunFunction fn_info;
+        if (auto error = Error(mun_runtime_find_function_definition(
+                m_handle, fn_name.data(), fn_name.size(), &has_fn, &fn_info));
+            error.is_error()) {
             if (out_error) {
                 *out_error = std::move(error);
             }
             return std::nullopt;
         }
 
-        return has_fn ? std::make_optional(std::move(fn_info)) : std::nullopt;
+        return has_fn ? std::make_optional(Function(fn_info)) : std::nullopt;
     }
 
     /**
@@ -69,12 +70,13 @@ class Runtime {
      *
      * \param type_info the type to allocate
      * \param out_error a pointer to fill with a potential error
-     * \return potentially, the handle of an allocated object
+     * \return potentially, the type_handle of an allocated object
      */
-    std::optional<MunGcPtr> gc_alloc(const TypeInfo& type_info,
+    std::optional<MunGcPtr> gc_alloc(const Type& type_info,
                                      Error* out_error = nullptr) const noexcept {
         MunGcPtr obj;
-        if (auto error = Error(mun_gc_alloc(m_handle, type_info.handle(), &obj))) {
+        if (auto error = Error(mun_gc_alloc(m_handle, type_info.type_handle(), &obj));
+            error.is_error()) {
             if (out_error) {
                 *out_error = std::move(error);
             }
@@ -91,9 +93,7 @@ class Runtime {
      */
     bool gc_collect() const noexcept {
         bool reclaimed;
-        auto error_handle = mun_gc_collect(m_handle, &reclaimed);
-        assert(error_handle._0 == nullptr);
-
+        MUN_ASSERT(mun_gc_collect(m_handle, &reclaimed));
         return reclaimed;
     }
 
@@ -105,14 +105,11 @@ class Runtime {
      * be collected. An object can be rooted multiple times, but you must make
      * sure to call `mun_gc_unroot` an equal number of times before the object
      * can be collected. If successful, `obj` has been rooted, otherwise a
-     * non-zero error handle is returned.
+     * non-zero error type_handle is returned.
      *
-     * \param obj a garbage collection handle
+     * \param obj a garbage collection type_handle
      */
-    void gc_root_ptr(MunGcPtr obj) const noexcept {
-        const auto error_handle = mun_gc_root(m_handle, obj);
-        assert(error_handle._0 == nullptr);
-    }
+    void gc_root_ptr(MunGcPtr obj) const noexcept { MUN_ASSERT(mun_gc_root(m_handle, obj)); }
 
     /**
      * Unroots the specified `obj`, potentially allowing it and objects it
@@ -122,24 +119,20 @@ class Runtime {
      * `gc_unroot_ptr` the same number of times as `gc_root_ptr` was called
      * before the object can be collected.
      *
-     * \param obj a garbage collection handle
+     * \param obj a garbage collection type_handle
      */
-    void gc_unroot_ptr(MunGcPtr obj) const noexcept {
-        const auto error_handle = mun_gc_unroot(m_handle, obj);
-        assert(error_handle._0 == nullptr);
-    }
+    void gc_unroot_ptr(MunGcPtr obj) const noexcept { MUN_ASSERT(mun_gc_unroot(m_handle, obj)); }
 
     /**
      * Retrieves the type information for the specified `obj`.
      *
-     * \param obj a garbage collection handle
-     * \return the handle's type information
+     * \param obj a garbage collection type_handle
+     * \return the type_handle's type information
      */
-    TypeInfo ptr_type(MunGcPtr obj) const noexcept {
-        MunTypeInfoHandle type_handle;
-        const auto error_handle = mun_gc_ptr_type(m_handle, obj, &type_handle);
-        assert(error_handle._0 == nullptr);
-        return TypeInfo(type_handle);
+    Type ptr_type(MunGcPtr obj) const noexcept {
+        MunType type_handle;
+        MUN_ASSERT(mun_gc_ptr_type(m_handle, obj, &type_handle));
+        return Type(type_handle);
     }
 
     /** Checks for updates to hot reloadable assemblies.
@@ -149,7 +142,7 @@ class Runtime {
      */
     bool update(Error* out_error = nullptr) {
         bool updated;
-        if (auto error = Error(mun_runtime_update(m_handle, &updated))) {
+        if (auto error = Error(mun_runtime_update(m_handle, &updated)); error.is_error()) {
             if (out_error) {
                 *out_error = std::move(error);
             }
@@ -159,7 +152,7 @@ class Runtime {
     }
 
 private:
-    MunRuntimeHandle m_handle;
+    MunRuntime m_handle;
 };
 
 struct RuntimeOptions {
@@ -194,13 +187,13 @@ inline std::optional<Runtime> make_runtime(std::string_view library_path,
         const auto& func = options.functions[i];
         definition = MunExternalFunctionDefinition{
             func.name.c_str(), static_cast<uint32_t>(func.arg_types.size()), func.arg_types.data(),
-            func.ret_type, func.fn_ptr};
+            func.ret_type.type_handle(), func.fn_ptr};
 
         // The MunExternalFunctionDefinition has ownership over the stored types
         for (const auto& arg_type : func.arg_types) {
-            mun_type_info_increment_strong_count(arg_type);
+            MUN_ASSERT(mun_type_add_reference(arg_type));
         }
-        mun_type_info_increment_strong_count(func.ret_type);
+        MUN_ASSERT(mun_type_add_reference(func.ret_type.type_handle()));
     }
 
     MunRuntimeOptions runtime_options;
@@ -208,8 +201,9 @@ inline std::optional<Runtime> make_runtime(std::string_view library_path,
         function_definitions.empty() ? nullptr : function_definitions.data();
     runtime_options.num_functions = static_cast<uint32_t>(function_definitions.size());
 
-    MunRuntimeHandle handle;
-    if (auto error = Error(mun_runtime_create(library_path.data(), runtime_options, &handle))) {
+    MunRuntime handle;
+    if (auto error = Error(mun_runtime_create(library_path.data(), runtime_options, &handle));
+        error.is_error()) {
         if (out_error) {
             *out_error = std::move(error);
         }
diff --git a/cpp/include/mun/runtime_capi.h b/cpp/include/mun/runtime_capi.h
index e99af28fa..0207d0a25 100644
--- a/cpp/include/mun/runtime_capi.h
+++ b/cpp/include/mun/runtime_capi.h
@@ -12,21 +12,21 @@ enum MunPrimitiveType
   : uint8_t
 #endif // __cplusplus
  {
-    MunPrimitiveType_Bool,
-    MunPrimitiveType_U8,
-    MunPrimitiveType_U16,
-    MunPrimitiveType_U32,
-    MunPrimitiveType_U64,
-    MunPrimitiveType_U128,
-    MunPrimitiveType_I8,
-    MunPrimitiveType_I16,
-    MunPrimitiveType_I32,
-    MunPrimitiveType_I64,
-    MunPrimitiveType_I128,
-    MunPrimitiveType_F32,
-    MunPrimitiveType_F64,
-    MunPrimitiveType_Empty,
-    MunPrimitiveType_Void,
+    MUN_PRIMITIVE_TYPE_BOOL,
+    MUN_PRIMITIVE_TYPE_U8,
+    MUN_PRIMITIVE_TYPE_U16,
+    MUN_PRIMITIVE_TYPE_U32,
+    MUN_PRIMITIVE_TYPE_U64,
+    MUN_PRIMITIVE_TYPE_U128,
+    MUN_PRIMITIVE_TYPE_I8,
+    MUN_PRIMITIVE_TYPE_I16,
+    MUN_PRIMITIVE_TYPE_I32,
+    MUN_PRIMITIVE_TYPE_I64,
+    MUN_PRIMITIVE_TYPE_I128,
+    MUN_PRIMITIVE_TYPE_F32,
+    MUN_PRIMITIVE_TYPE_F64,
+    MUN_PRIMITIVE_TYPE_EMPTY,
+    MUN_PRIMITIVE_TYPE_VOID,
 };
 #ifndef __cplusplus
 typedef uint8_t MunPrimitiveType;
@@ -44,46 +44,42 @@ enum MunStructMemoryKind
      * A garbage collected struct is allocated on the heap and uses reference semantics when passed
      * around.
      */
-    Gc,
+    MUN_STRUCT_MEMORY_KIND_GC,
     /**
      * A value struct is allocated on the stack and uses value semantics when passed around.
      *
      * NOTE: When a value struct is used in an external API, a wrapper is created that _pins_ the
      * value on the heap. The heap-allocated value needs to be *manually deallocated*!
      */
-    Value,
+    MUN_STRUCT_MEMORY_KIND_VALUE,
 };
 #ifndef __cplusplus
 typedef uint8_t MunStructMemoryKind;
 #endif // __cplusplus
 
-/**
- * A linked version of [`mun_abi::StructInfo`] that has resolved all occurrences of `TypeId` with `TypeInfo`.
- */
-typedef struct MunStructInfo MunStructInfo;
-
 /**
  * A C-style handle to an error message.
  *
  * If the handle contains a non-null pointer, an error occurred.
  */
 typedef struct MunErrorHandle {
-    const char *_0;
+    const char *error_string;
 } MunErrorHandle;
 
 /**
  * A C-style handle to a runtime.
  */
-typedef struct MunRuntimeHandle {
+typedef struct MunRuntime {
     void *_0;
-} MunRuntimeHandle;
+} MunRuntime;
 
 /**
- * A C-style handle to a `TypeInfo`.
+ * A [`Type`] holds information about a mun type.
  */
-typedef struct MunTypeInfoHandle {
+typedef struct MunType {
     const void *_0;
-} MunTypeInfoHandle;
+    const void *_1;
+} MunType;
 
 /**
  * A `RawGcPtr` is an unsafe version of a `GcPtr`. It represents the raw internal pointer
@@ -118,11 +114,11 @@ typedef struct MunExternalFunctionDefinition {
     /**
      * The types of the arguments
      */
-    const struct MunTypeInfoHandle *arg_types;
+    const struct MunType *arg_types;
     /**
      * The type of the return type
      */
-    struct MunTypeInfoHandle return_type;
+    struct MunType return_type;
     /**
      * Pointer to the function
      */
@@ -153,11 +149,17 @@ typedef struct MunRuntimeOptions {
 } MunRuntimeOptions;
 
 /**
- * A C-style handle to a `FunctionInfo`.
+ * Describes a `Function` accessible from a Mun [`super::runtime::Runtime`].
+ *
+ * An instance of `Function` shares ownership of the underlying data. To create a copy of the
+ * `Function` object call [`mun_function_add_reference`] to make sure the number of references to
+ * the data is properly tracked. Calling [`mun_function_release`] signals the runtime that the data
+ * is no longer referenced through the specified object. When all references are released the
+ * underlying data is deallocated.
  */
-typedef struct MunFunctionInfoHandle {
+typedef struct MunFunction {
     const void *_0;
-} MunFunctionInfoHandle;
+} MunFunction;
 
 /**
  * Represents a globally unique identifier (GUID).
@@ -194,11 +196,11 @@ enum MunTypeId_Tag
     /**
      * Represents a concrete type with a specific Guid
      */
-    Concrete,
+    MUN_TYPE_ID_CONCRETE,
     /**
      * Represents a pointer to a type
      */
-    Pointer,
+    MUN_TYPE_ID_POINTER,
 };
 #ifndef __cplusplus
 typedef uint8_t MunTypeId_Tag;
@@ -217,121 +219,101 @@ typedef union MunTypeId {
 } MunTypeId;
 
 /**
- * A C-style handle to a `FieldInfo`.
- */
-typedef struct MunFieldInfoHandle {
-    const void *_0;
-} MunFieldInfoHandle;
-
-/**
- * A C-style handle to an array of `FieldInfoHandle`s.
- */
-typedef struct MunFieldInfoSpan {
-    /**
-     * Pointer to the start of the array buffer
-     */
-    const struct MunFieldInfoHandle *data;
-    /**
-     * Length of the array (and capacity)
-     */
-    uintptr_t len;
-} MunFieldInfoSpan;
-
-/**
- * A C-style handle to an array of `TypeInfoHandle`s.
+ * An array of [`Type`]s.
+ *
+ * The `Types` struct owns the `Type`s it references. Ownership of the `Type` can be shared by
+ * calling [`mun_type_add_reference`].
+ *
+ * This is backed by a dynamically allocated array. Ownership is transferred via this struct
+ * and its contents must be destroyed with [`mun_types_destroy`].
  */
-typedef struct MunTypeInfoSpan {
-    /**
-     * Pointer to the start of the array buffer
-     */
-    const struct MunTypeInfoHandle *data;
-    /**
-     * Length of the array (and capacity)
-     */
-    uintptr_t len;
-} MunTypeInfoSpan;
+typedef struct MunTypes {
+    const struct MunType *types;
+    uintptr_t count;
+} MunTypes;
 
 /**
- * A C-style handle to a `StructInfo`.
+ * Additional information of a pointer [`Type`].
+ *
+ * Ownership of this type lies with the [`Type`] that created this instance. As long as the
+ * original type is not released through [`mun_type_release`] this type stays alive.
  */
-typedef struct MunStructInfoHandle {
+typedef struct MunPointerInfo {
     const void *_0;
-} MunStructInfoHandle;
+    const void *_1;
+} MunPointerInfo;
 
 /**
- * A pointer to another type.
+ * Additional information of a struct [`Type`].
+ *
+ * Ownership of this type lies with the [`Type`] that created this instance. As long as the
+ * original type is not released through [`mun_type_release`] this type stays alive.
  */
-typedef struct MunPointerInfoData {
-    bool mutable_;
-} MunPointerInfoData;
+typedef struct MunStructInfo {
+    const void *_0;
+    const void *_1;
+} MunStructInfo;
 
 /**
- * An enum containing C-style handles a `TypeInfo`'s data.
+ * An enum that defines the kind of type.
  */
-enum MunTypeInfoData_Tag
+enum MunTypeKind_Tag
 #ifdef __cplusplus
   : uint8_t
 #endif // __cplusplus
  {
-    /**
-     * Primitive types (i.e. `()`, `bool`, `float`, `int`, etc.)
-     */
-    MunTypeInfoData_Primitive,
-    /**
-     * Struct types (i.e. record, tuple, or unit structs)
-     */
-    MunTypeInfoData_Struct,
-    /**
-     * A pointer type
-     */
-    MunTypeInfoData_Pointer,
+    MUN_TYPE_KIND_PRIMITIVE,
+    MUN_TYPE_KIND_POINTER,
+    MUN_TYPE_KIND_STRUCT,
 };
 #ifndef __cplusplus
-typedef uint8_t MunTypeInfoData_Tag;
+typedef uint8_t MunTypeKind_Tag;
 #endif // __cplusplus
 
-typedef union MunTypeInfoData {
-    MunTypeInfoData_Tag tag;
+typedef union MunTypeKind {
+    MunTypeKind_Tag tag;
     struct {
-        MunTypeInfoData_Tag primitive_tag;
+        MunTypeKind_Tag primitive_tag;
         struct MunGuid primitive;
     };
     struct {
-        MunTypeInfoData_Tag struct_tag;
-        struct MunStructInfoHandle struct_;
+        MunTypeKind_Tag pointer_tag;
+        struct MunPointerInfo pointer;
     };
     struct {
-        MunTypeInfoData_Tag pointer_tag;
-        struct MunPointerInfoData pointer;
+        MunTypeKind_Tag struct_tag;
+        struct MunStructInfo struct_;
     };
-} MunTypeInfoData;
-
-#ifdef __cplusplus
-extern "C" {
-#endif // __cplusplus
+} MunTypeKind;
 
 /**
- * Deallocates a string that was allocated by the runtime.
+ * Information of a field of a struct [`Type`].
  *
- * # Safety
- *
- * This function receives a raw pointer as parameter. Only when the argument is not a null pointer,
- * its content will be deallocated. Passing pointers to invalid data or memory allocated by other
- * processes, will lead to undefined behavior.
+ * Ownership of this type lies with the [`Type`] that created this instance. As long as the
+ * original type is not released through [`mun_type_release`] this type stays alive.
  */
-void mun_string_destroy(const char *string);
+typedef struct MunField {
+    const void *_0;
+    const void *_1;
+} MunField;
 
 /**
- * Destructs the error message corresponding to the specified handle.
- *
- * # Safety
+ * An array of [`Field`]s.
  *
- * Only call this function on an ErrorHandle once.
+ * This is backed by a dynamically allocated array. Ownership is transferred via this struct
+ * and its contents must be destroyed with [`mun_fields_destroy`].
  */
-void mun_error_destroy(struct MunErrorHandle error);
+typedef struct MunFields {
+    const struct MunField *fields;
+    uintptr_t count;
+} MunFields;
+
+#ifdef __cplusplus
+extern "C" {
+#endif // __cplusplus
 
 /**
- * Allocates an object in the runtime of the given `type_info`. If successful, `obj` is set,
+ * Allocates an object in the runtime of the given `ty`. If successful, `obj` is set,
  * otherwise a non-zero error handle is returned.
  *
  * If a non-zero error handle is returned, it must be manually destructed using
@@ -342,13 +324,11 @@ void mun_error_destroy(struct MunErrorHandle error);
  * This function receives raw pointers as parameters. If any of the arguments is a null pointer,
  * an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
  */
-struct MunErrorHandle mun_gc_alloc(struct MunRuntimeHandle runtime,
-                                   struct MunTypeInfoHandle type_info,
-                                   MunGcPtr *obj);
+struct MunErrorHandle mun_gc_alloc(struct MunRuntime runtime, struct MunType ty, MunGcPtr *obj);
 
 /**
- * Retrieves the `type_info` for the specified `obj` from the runtime. If successful, `type_info`
- * is set, otherwise a non-zero error handle is returned.
+ * Retrieves the `ty` for the specified `obj` from the runtime. If successful, `ty` is set,
+ * otherwise a non-zero error handle is returned.
  *
  * If a non-zero error handle is returned, it must be manually destructed using
  * [`mun_error_destroy`].
@@ -358,9 +338,7 @@ struct MunErrorHandle mun_gc_alloc(struct MunRuntimeHandle runtime,
  * This function receives raw pointers as parameters. If any of the arguments is a null pointer,
  * an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
  */
-struct MunErrorHandle mun_gc_ptr_type(struct MunRuntimeHandle handle,
-                                      MunGcPtr obj,
-                                      struct MunTypeInfoHandle *type_info);
+struct MunErrorHandle mun_gc_ptr_type(struct MunRuntime runtime, MunGcPtr obj, struct MunType *ty);
 
 /**
  * Roots the specified `obj`, which keeps it and objects it references alive. Objects marked as
@@ -377,7 +355,7 @@ struct MunErrorHandle mun_gc_ptr_type(struct MunRuntimeHandle handle,
  * This function receives raw pointers as parameters. If any of the arguments is a null pointer,
  * an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
  */
-struct MunErrorHandle mun_gc_root(struct MunRuntimeHandle handle, MunGcPtr obj);
+struct MunErrorHandle mun_gc_root(struct MunRuntime runtime, MunGcPtr obj);
 
 /**
  * Unroots the specified `obj`, potentially allowing it and objects it references to be
@@ -393,7 +371,7 @@ struct MunErrorHandle mun_gc_root(struct MunRuntimeHandle handle, MunGcPtr obj);
  * This function receives raw pointers as parameters. If any of the arguments is a null pointer,
  * an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
  */
-struct MunErrorHandle mun_gc_unroot(struct MunRuntimeHandle handle, MunGcPtr obj);
+struct MunErrorHandle mun_gc_unroot(struct MunRuntime runtime, MunGcPtr obj);
 
 /**
  * Collects all memory that is no longer referenced by rooted objects. If successful, `reclaimed`
@@ -408,7 +386,7 @@ struct MunErrorHandle mun_gc_unroot(struct MunRuntimeHandle handle, MunGcPtr obj
  * This function receives raw pointers as parameters. If any of the arguments is a null pointer,
  * an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
  */
-struct MunErrorHandle mun_gc_collect(struct MunRuntimeHandle handle, bool *reclaimed);
+struct MunErrorHandle mun_gc_collect(struct MunRuntime runtime, bool *reclaimed);
 
 /**
  * Constructs a new runtime that loads the library at `library_path` and its dependencies. If
@@ -426,17 +404,16 @@ struct MunErrorHandle mun_gc_collect(struct MunRuntimeHandle handle, bool *recla
  */
 struct MunErrorHandle mun_runtime_create(const char *library_path,
                                          struct MunRuntimeOptions options,
-                                         struct MunRuntimeHandle *handle);
+                                         struct MunRuntime *handle);
 
 /**
  * Destructs the runtime corresponding to `handle`.
  */
-void mun_runtime_destroy(struct MunRuntimeHandle handle);
+struct MunErrorHandle mun_runtime_destroy(struct MunRuntime runtime);
 
 /**
- * Retrieves the [`FunctionDefinition`] for `fn_name` from the runtime corresponding to `handle`.
- * If successful, `has_fn_info` and `fn_info` are set, otherwise a non-zero error handle is
- * returned.
+ * Retrieves the [`FunctionDefinition`] for `fn_name` from the `runtime`. If successful,
+ * `has_fn_info` and `fn_info` are set, otherwise a non-zero error handle is returned.
  *
  * If a non-zero error handle is returned, it must be manually destructed using
  * [`mun_error_destroy`].
@@ -446,10 +423,11 @@ void mun_runtime_destroy(struct MunRuntimeHandle handle);
  * This function receives raw pointers as parameters. If any of the arguments is a null pointer,
  * an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
  */
-struct MunErrorHandle mun_runtime_get_function_info(struct MunRuntimeHandle handle,
-                                                    const char *fn_name,
-                                                    bool *has_fn_info,
-                                                    struct MunFunctionInfoHandle *fn_info);
+struct MunErrorHandle mun_runtime_find_function_definition(struct MunRuntime runtime,
+                                                           const char *fn_name,
+                                                           uintptr_t fn_name_len,
+                                                           bool *has_fn_info,
+                                                           struct MunFunction *fn_info);
 
 /**
  * Retrieves the type information corresponding to the specified `type_name` from the runtime.
@@ -464,10 +442,10 @@ struct MunErrorHandle mun_runtime_get_function_info(struct MunRuntimeHandle hand
  * This function receives raw pointers as parameters. If any of the arguments is a null pointer,
  * an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
  */
-struct MunErrorHandle mun_runtime_get_type_info_by_name(struct MunRuntimeHandle runtime,
+struct MunErrorHandle mun_runtime_get_type_info_by_name(struct MunRuntime runtime,
                                                         const char *type_name,
                                                         bool *has_type_info,
-                                                        struct MunTypeInfoHandle *type_info);
+                                                        struct MunType *type_info);
 
 /**
  * Retrieves the type information corresponding to the specified `type_id` from the runtime. If
@@ -482,10 +460,10 @@ struct MunErrorHandle mun_runtime_get_type_info_by_name(struct MunRuntimeHandle
  * This function receives raw pointers as parameters. If any of the arguments is a null pointer,
  * an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
  */
-struct MunErrorHandle mun_runtime_get_type_info_by_id(struct MunRuntimeHandle runtime,
+struct MunErrorHandle mun_runtime_get_type_info_by_id(struct MunRuntime runtime,
                                                       const union MunTypeId *type_id,
                                                       bool *has_type_info,
-                                                      struct MunTypeInfoHandle *type_info);
+                                                      struct MunType *type_info);
 
 /**
  * Updates the runtime corresponding to `handle`. If successful, `updated` is set, otherwise a
@@ -499,245 +477,294 @@ struct MunErrorHandle mun_runtime_get_type_info_by_id(struct MunRuntimeHandle ru
  * This function receives raw pointers as parameters. If any of the arguments is a null pointer,
  * an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
  */
-struct MunErrorHandle mun_runtime_update(struct MunRuntimeHandle handle, bool *updated);
+struct MunErrorHandle mun_runtime_update(struct MunRuntime runtime, bool *updated);
 
 /**
- * Retrieves the field's name.
+ * Notifies the runtime an additional references exists to the function. This ensures that the data
+ * is kept alive even if [`mun_function_release`] is called for the existing references. Only
+ * after all references have been released can the underlying data be deallocated.
  *
  * # Safety
  *
- * The caller is responsible for calling `mun_string_destroy` on the return pointer - if it is not null.
- *
- * This function might result in undefined behavior if the [`crate::TypeInfoHandle`] associated
- * with this `FieldInfoHandle` has been deallocated.
+ * This function might be unsafe if the underlying data has already been deallocated by a previous
+ * call to [`mun_function_release`].
  */
-const char *mun_field_info_name(struct MunFieldInfoHandle field_info);
+struct MunErrorHandle mun_function_add_reference(struct MunFunction function);
 
 /**
- * Retrieves the field's type.
+ * Notifies the runtime that one of the references to the function is no longer in use. The data
+ * may not immediately be destroyed. Only after all references have been released can the
+ * underlying data be deallocated.
  *
  * # Safety
  *
- * This method is considered unsafe because the passed `field_info` might have been deallocated by
- * a call to [`mun_type_info_decrement_strong_count`] of the type that contains this field.
+ * This function might be unsafe if the underlying data has been deallocated by a previous call
+ * to [`mun_function_release`].
  */
-struct MunTypeInfoHandle mun_field_info_type(struct MunFieldInfoHandle field_info);
+struct MunErrorHandle mun_function_release(struct MunFunction function);
 
 /**
- * Retrieves the field's offset.
+ * Retrieves the function's function pointer.
  *
  * # Safety
  *
- * This method is considered unsafe because the passed `field_info` might have been deallocated by
- * a call to [`mun_type_info_decrement_strong_count`] of the type that contains this field.
+ * This function might be unsafe if the underlying data has been deallocated by a previous call
+ * to [`mun_function_release`].
  */
-struct MunErrorHandle mun_field_info_offset(struct MunFieldInfoHandle field_info,
-                                            uint16_t *field_offset);
+struct MunErrorHandle mun_function_fn_ptr(struct MunFunction function, const void **ptr);
 
 /**
- * Deallocates a span of `FieldInfo`s that was allocated by the runtime.
+ * Retrieves the function's name.
  *
- * Deallocating span only deallocates the data allocated for the span. Deallocating a span will not
- * deallocate the FieldInfo's it references. `FieldInfo`s are destroyed when the top-level
- * `TypeInfo` is destroyed.
+ * If the function is successful, the caller is responsible for calling [`mun_string_destroy`] on
+ * the return pointer.
  *
  * # Safety
  *
- * This function receives a span as parameter. Only when the spans data pointer is not null, is the
- * content deallocated. Passing pointers to invalid data of memory allocated by other processes,
- * will lead to undefined behavior.
+ * This function might be unsafe if the underlying data has been deallocated by a previous call
+ * to [`mun_function_release`].
  */
-bool mun_field_info_span_destroy(struct MunFieldInfoSpan span);
+struct MunErrorHandle mun_function_name(struct MunFunction function, const char **name);
 
 /**
- * Decrements the strong count of the `Arc<FunctionDefinition>` associated with `handle`.
+ * Retrieves the function's argument types.
+ *
+ * If successful, ownership of the [`Types`] is transferred to the caller. It must be deallocated
+ * with a call to [`mun_types_destroy`].
  *
  * # Safety
  *
- * This function might be unsafe if the underlying data has already been deallocated by a previous
- * call to [`mun_function_info_decrement_strong_count`].
+ *
+ * This function might be unsafe if the underlying data has been deallocated by a previous call
+ * to [`mun_function_release`].
  */
-bool mun_function_info_decrement_strong_count(struct MunFunctionInfoHandle fn_info);
+struct MunErrorHandle mun_function_argument_types(struct MunFunction function,
+                                                  struct MunTypes *arg_types);
 
 /**
- * Increments the strong count of the `Arc<FunctionDefinition>` associated with `handle`.
+ * Retrieves the function's return type.
+ *
+ * Ownership of the [`Type`] is transferred to the called. It must be released with a call to
+ * [`mun_type_release`].
  *
  * # Safety
  *
  * This function might be unsafe if the underlying data has been deallocated by a previous call
- * to [`mun_function_info_decrement_strong_count`].
+ * to [`mun_function_release`].
  */
-bool mun_function_info_increment_strong_count(struct MunFunctionInfoHandle fn_info);
+struct MunErrorHandle mun_function_return_type(struct MunFunction function, struct MunType *ty);
 
 /**
- * Retrieves the function's function pointer.
+ * Deallocates a string that was allocated by the runtime.
  *
  * # Safety
  *
- * This function might be unsafe if the underlying data has been deallocated by a previous call
- * to [`mun_function_info_decrement_strong_count`].
+ * This function receives a raw pointer as parameter. Only when the argument is not a null pointer,
+ * its content will be deallocated. Passing pointers to invalid data or memory allocated by other
+ * processes, will lead to undefined behavior.
  */
-const void *mun_function_info_fn_ptr(struct MunFunctionInfoHandle fn_info);
+void mun_string_destroy(const char *string);
 
 /**
- * Retrieves the function's name.
+ * Destructs the error message corresponding to the specified handle.
  *
  * # Safety
  *
- * The caller is responsible for calling `mun_string_destroy` on the return pointer - if it is not null.
- *
- * This function might be unsafe if the underlying data has been deallocated by a previous call
- * to [`mun_function_info_decrement_strong_count`].
+ * Only call this function on an ErrorHandle once.
  */
-const char *mun_function_info_name(struct MunFunctionInfoHandle fn_info);
+void mun_error_destroy(struct MunErrorHandle error);
 
 /**
- * Retrieves the function's argument types.
+ * Notifies the runtime that the specified type is no longer used. Any use of the type after
+ * calling this function results in undefined behavior.
  *
  * # Safety
  *
- * If a non-null handle is returned, the caller is responsible for calling
- * `mun_type_info_span_destroy` on the returned handle.
+ * This function results in undefined behavior if the passed in `Type` has been deallocated in a
+ * previous call to [`mun_type_release`].
+ */
+struct MunErrorHandle mun_type_release(struct MunType ty);
+
+/**
+ * Increments the usage count of the specified type.
+ *
+ * # Safety
  *
- * This function might be unsafe if the underlying data has been deallocated by a previous call
- * to [`mun_function_info_decrement_strong_count`].
+ * This function results in undefined behavior if the passed in `Type` has been deallocated in a
+ * previous call to [`mun_type_release`].
  */
-struct MunErrorHandle mun_function_info_argument_types(struct MunFunctionInfoHandle fn_info,
-                                                       struct MunTypeInfoSpan *arg_types);
+struct MunErrorHandle mun_type_add_reference(struct MunType ty);
 
 /**
- * Retrieves the function's return type.
+ * Retrieves the type's name.
  *
  * # Safety
  *
- * This function might be unsafe if the underlying data has been deallocated by a previous call
- * to [`mun_function_info_decrement_strong_count`].
+ * The caller is responsible for calling `mun_string_destroy` on the return pointer - if it is not
+ * null.
+ *
+ * This function results in undefined behavior if the passed in `Type` has been deallocated in a
+ * previous call to [`mun_type_release`].
  */
-struct MunTypeInfoHandle mun_function_info_return_type(struct MunFunctionInfoHandle fn_info);
+struct MunErrorHandle mun_type_name(struct MunType ty, const char **name);
 
 /**
- * Retrieves information about the struct's fields.
+ * Compares two different Types. Returns `true` if the two types are equal. If either of the two
+ * types is invalid because for instance it contains null pointers this function returns `false`.
  *
  * # Safety
  *
- * The caller is responsible for calling `mun_field_info_span_destroy` on the returned span.
+ * This function results in undefined behavior if the passed in `Type`s have been deallocated in a
+ * previous call to [`mun_type_release`].
+ */
+bool mun_type_equal(struct MunType a, struct MunType b);
+
+/**
+ * Returns the storage size required for a type. The storage size does not include any padding to
+ * align the size. Call [`mun_type_alignment`] to request the alignment of the type.
  *
- * This function might result in undefined behavior if the [`crate::TypeInfoHandle`] associated
- * with this `StructInfoHandle` has been deallocated.
+ * # Safety
+ *
+ * This function results in undefined behavior if the passed in `Type`s have been deallocated in a
+ * previous call to [`mun_type_release`].
  */
-struct MunErrorHandle mun_struct_info_fields(struct MunStructInfoHandle struct_info,
-                                             struct MunFieldInfoSpan *field_info_span);
+struct MunErrorHandle mun_type_size(struct MunType ty, uintptr_t *size);
 
 /**
- * Retrieves the struct's memory kind.
+ * Returns the alignment requirements of the type.
  *
  * # Safety
  *
- * This function might result in undefined behavior if the [`crate::TypeInfoHandle`] associated
- * with this `StructInfoHandle` has been deallocated.
+ * This function results in undefined behavior if the passed in `Type`s have been deallocated in a
+ * previous call to [`mun_type_release`].
  */
-struct MunErrorHandle mun_struct_info_memory_kind(struct MunStructInfoHandle struct_info,
-                                                  MunStructMemoryKind *memory_kind);
+struct MunErrorHandle mun_type_alignment(struct MunType ty, uintptr_t *align);
 
 /**
- * Decrements the strong count of the `Arc<TypeInfo>` associated with `handle`.
+ * Returns a new [`Type`] that is a pointer to the specified type.
  *
  * # Safety
  *
- * This function results in undefined behavior if the passed in `TypeInfoHandle` has already been
- * deallocated in a previous call to [`mun_type_info_decrement_strong_count`].
+ * This function results in undefined behavior if the passed in `Type`s have been deallocated in a
+ * previous call to [`mun_type_release`].
  */
-bool mun_type_info_decrement_strong_count(struct MunTypeInfoHandle handle);
+struct MunErrorHandle mun_type_pointer_type(struct MunType ty,
+                                            bool mutable_,
+                                            struct MunType *pointer_ty);
 
 /**
- * Increments the strong count of the `Arc<TypeInfo>` associated with `handle`.
+ * Returns information about what kind of type this is.
  *
  * # Safety
  *
- * This function results in undefined behavior if the passed in `TypeInfoHandle` has already been
- * deallocated in a previous call to [`mun_type_info_decrement_strong_count`].
+ * This function results in undefined behavior if the passed in `Type`s have been deallocated in a
+ * previous call to [`mun_type_release`].
  */
-bool mun_type_info_increment_strong_count(struct MunTypeInfoHandle handle);
+struct MunErrorHandle mun_type_kind(struct MunType ty, union MunTypeKind *kind);
 
 /**
- * Retrieves the type's name.
+ * Destroys the contents of a [`Types`] struct.
  *
  * # Safety
  *
- * The caller is responsible for calling `mun_string_destroy` on the return pointer - if it is not null.
+ * This function results in undefined behavior if the passed in `Types` has been deallocated
+ * by a previous call to [`mun_types_destroy`].
+ */
+struct MunErrorHandle mun_types_destroy(struct MunTypes types);
+
+/**
+ * Returns a [`Type`] that represents the specified primitive type.
+ */
+struct MunType mun_type_primitive(MunPrimitiveType primitive_type);
+
+/**
+ * Returns the globally unique identifier (GUID) of the struct.
+ *
+ * # Safety
  *
- * This function results in undefined behavior if the passed in `TypeInfoHandle` has been
- * deallocated in a previous call to [`mun_type_info_decrement_strong_count`].
+ * This function results in undefined behavior if the passed in `StructType` has been deallocated
+ * by a previous call to [`mun_type_release`].
  */
-const char *mun_type_info_name(struct MunTypeInfoHandle type_info);
+struct MunErrorHandle mun_struct_type_guid(struct MunStructInfo ty, struct MunGuid *guid);
 
 /**
- * Returns true if the specified type info handles describe the same type.
+ * Returns the type of memory management to apply for the struct.
  *
  * # Safety
  *
- * This function results in undefined behavior if any of the the passed in `TypeInfoHandle` have
- * been deallocated in a previous call to [`mun_type_info_decrement_strong_count`].
+ * This function results in undefined behavior if the passed in `StructType` has been deallocated
+ * by a previous call to [`mun_type_release`].
  */
-bool mun_type_info_eq(struct MunTypeInfoHandle a, struct MunTypeInfoHandle b);
+struct MunErrorHandle mun_struct_type_memory_kind(struct MunStructInfo ty,
+                                                  MunStructMemoryKind *memory_kind);
 
 /**
- * Returns the TypeInfoHandle of a pointer to the given TypeInfoHandle.
+ * Retrieves the field with the given name.
+ *
+ * The name can be passed as a non nul-terminated string it must be UTF-8 encoded.
  *
  * # Safety
  *
- * This function results in undefined behavior if any of the the passed in `TypeInfoHandle` have
- * been deallocated in a previous call to [`mun_type_info_decrement_strong_count`].
+ * This function results in undefined behavior if the passed in `Fields` has been deallocated
+ * by a previous call to [`mun_fields_destroy`].
  */
-struct MunTypeInfoHandle mun_type_info_pointer_type(struct MunTypeInfoHandle handle, bool mutable_);
+struct MunErrorHandle mun_fields_find_by_name(struct MunFields fields,
+                                              const char *name,
+                                              uintptr_t len,
+                                              bool *has_field,
+                                              struct MunField *field);
 
 /**
- * Retrieves the type's size.
+ * Destroys the contents of a [`Fields`] struct.
  *
  * # Safety
  *
- * This function results in undefined behavior if the passed in `TypeInfoHandle` has been
- * deallocated in a previous call to [`mun_type_info_decrement_strong_count`].
+ * This function results in undefined behavior if the passed in `Fields` has been deallocated
+ * by a previous call to [`mun_fields_destroy`].
  */
-struct MunErrorHandle mun_type_info_size(struct MunTypeInfoHandle type_info, uintptr_t *size);
+struct MunErrorHandle mun_fields_destroy(struct MunFields fields);
 
 /**
- * Retrieves the type's alignment.
+ * Retrieves all the fields of the specified struct type.
  *
  * # Safety
  *
- * This function results in undefined behavior if the passed in `TypeInfoHandle` has been
- * deallocated in a previous call to [`mun_type_info_decrement_strong_count`].
+ * This function results in undefined behavior if the passed in `StructType` has been deallocated
+ * by a previous call to [`mun_type_release`].
  */
-struct MunErrorHandle mun_type_info_align(struct MunTypeInfoHandle type_info, uintptr_t *align);
+struct MunErrorHandle mun_struct_type_fields(struct MunStructInfo ty, struct MunFields *fields);
 
 /**
- * Retrieves the type's data.
+ * Returns the name of the field in the parent struct. Ownership of the name is transferred and
+ * must be destroyed with [`mun_string_destroy`]. If this function fails a nullptr is returned.
  *
  * # Safety
  *
- * The original `TypeInfoHandle` needs to stay alive as long as the `TypeInfoData` lives. The
- * `TypeInfoData` is destroyed at the same time as the `TypeInfo`. A `TypeInfo` might be destroyed
- * through a call to [`mun_type_info_decrement_strong_count`].
+ * This function results in undefined behavior if the passed in `Field` has been deallocated
+ * by a previous call to [`mun_type_release`].
  */
-struct MunErrorHandle mun_type_info_data(struct MunTypeInfoHandle type_info,
-                                         union MunTypeInfoData *type_info_data);
+struct MunErrorHandle mun_field_name(struct MunField field, const char **name);
 
 /**
- * Deallocates an span of `TypeInfo` that was allocated by the runtime.
+ * Returns the type of the field. Ownership of the returned [`Type`] is transferred and must be
+ * released with a call to [`mun_type_release`].
  *
  * # Safety
  *
- * This function receives a span as parameter. Only when the spans data pointer is not null, its
- * content will be deallocated. Passing pointers to invalid data or memory allocated by other
- * processes, will lead to undefined behavior.
+ * This function results in undefined behavior if the passed in `Field` has been deallocated
+ * by a previous call to [`mun_type_release`].
  */
-bool mun_type_info_span_destroy(struct MunTypeInfoSpan array_handle);
+struct MunErrorHandle mun_field_type(struct MunField field, struct MunType *ty);
 
 /**
- * Returns a TypeInfoHandle that represents the specified primitive type.
+ * Returns the offset of the field in bytes from the start of the parent struct.
+ *
+ * # Safety
+ *
+ * This function results in undefined behavior if the passed in `Field` has been deallocated
+ * by a previous call to [`mun_type_release`].
  */
-struct MunTypeInfoHandle mun_type_info_primitive(MunPrimitiveType primitive_type);
+struct MunErrorHandle mun_field_offset(struct MunField field, uintptr_t *offset);
 
 #ifdef __cplusplus
 } // extern "C"
diff --git a/cpp/include/mun/runtime_function.h b/cpp/include/mun/runtime_function.h
index 74ea00817..a36201da8 100644
--- a/cpp/include/mun/runtime_function.h
+++ b/cpp/include/mun/runtime_function.h
@@ -6,7 +6,7 @@
 
 #include "mun/runtime_capi.h"
 #include "mun/static_type_info.h"
-#include "mun/type_info.h"
+#include "mun/type.h"
 #include "mun/util.h"
 
 namespace mun {
@@ -22,14 +22,10 @@ struct RuntimeFunction {
     template <typename... TArgs>
     RuntimeFunction(std::string_view name, void(MUN_CALLTYPE* fn_ptr)(TArgs...))
         : name(name),
-          arg_types({StaticTypeInfo<TArgs>::type_info().handle()...}),
-          ret_type(StaticTypeInfo<std::tuple<>>::type_info().handle()),
-          fn_ptr(reinterpret_cast<const void*>(fn_ptr)) {
-        for (const auto& arg_type : arg_types) {
-            mun_type_info_increment_strong_count(arg_type);
-        }
-        mun_type_info_increment_strong_count(ret_type);
-    }
+          arg_types(
+              {Type(StaticTypeInfo<TArgs>::type_info().type_handle()).release_type_handle()...}),
+          ret_type(StaticTypeInfo<std::tuple<>>::type_info().type_handle()),
+          fn_ptr(reinterpret_cast<const void*>(fn_ptr)) {}
 
     /**
      * Constructs a `RuntimeFunction` from a generic function pointer and a name.
@@ -39,30 +35,45 @@ struct RuntimeFunction {
     template <typename TRet, typename... TArgs>
     RuntimeFunction(std::string_view name, TRet(MUN_CALLTYPE* fn_ptr)(TArgs...))
         : name(name),
-          arg_types({StaticTypeInfo<TArgs>::type_info().handle()...}),
-          ret_type(StaticTypeInfo<TRet>::type_info().handle()),
-          fn_ptr(reinterpret_cast<const void*>(fn_ptr)) {
+          arg_types(
+              {Type(StaticTypeInfo<TArgs>::type_info().type_handle()).release_type_handle()...}),
+          ret_type(StaticTypeInfo<TRet>::type_info().type_handle()),
+          fn_ptr(reinterpret_cast<const void*>(fn_ptr)) {}
+
+    ~RuntimeFunction() {
         for (const auto& arg_type : arg_types) {
-            mun_type_info_increment_strong_count(arg_type);
+            MUN_ASSERT(mun_type_release(arg_type));
         }
-        mun_type_info_increment_strong_count(ret_type);
     }
 
-    ~RuntimeFunction() {
+    RuntimeFunction(const RuntimeFunction& other)
+        : name(other.name),
+          arg_types(other.arg_types),
+          ret_type(other.ret_type),
+          fn_ptr(other.fn_ptr) {
         for (const auto& arg_type : arg_types) {
-            mun_type_info_decrement_strong_count(arg_type);
+            MUN_ASSERT(mun_type_add_reference(arg_type));
         }
-        mun_type_info_decrement_strong_count(ret_type);
     }
+    RuntimeFunction(RuntimeFunction&& other) = default;
 
-    RuntimeFunction(const RuntimeFunction&) = default;
-    RuntimeFunction(RuntimeFunction&&) = default;
-    RuntimeFunction& operator=(const RuntimeFunction&) = default;
+    RuntimeFunction& operator=(const RuntimeFunction& other) {
+        name = other.name;
+        for (const auto& arg_type : other.arg_types) {
+            MUN_ASSERT(mun_type_add_reference(arg_type));
+        }
+        for (const auto& arg_type : arg_types) {
+            MUN_ASSERT(mun_type_release(arg_type));
+        }
+        arg_types = other.arg_types;
+        ret_type = other.ret_type;
+        fn_ptr = other.fn_ptr;
+    };
     RuntimeFunction& operator=(RuntimeFunction&&) = default;
 
     std::string name;
-    std::vector<MunTypeInfoHandle> arg_types;
-    MunTypeInfoHandle ret_type;
+    std::vector<MunType> arg_types;
+    Type ret_type;
     const void* fn_ptr;
 };
 }  // namespace mun
diff --git a/cpp/include/mun/static_type_info.h b/cpp/include/mun/static_type_info.h
index 2c0e77c0f..c845227c9 100644
--- a/cpp/include/mun/static_type_info.h
+++ b/cpp/include/mun/static_type_info.h
@@ -2,37 +2,37 @@
 #define MUN_STATIC_TYPE_INFO_H_
 
 #include "mun/runtime_capi.h"
-#include "mun/type_info.h"
+#include "mun/type.h"
 
 namespace mun {
 
 template <typename T>
 struct StaticTypeInfo;
 
-#define IMPL_PRIMITIVE_TYPE_INFO(ty, mun_primitive_type)                                       \
-    template <>                                                                                \
-    struct StaticTypeInfo<ty> {                                                                \
-        static const TypeInfo& type_info() {                                                   \
-            static TypeInfo TYPE_INFO = TypeInfo(mun_type_info_primitive(mun_primitive_type)); \
-            return TYPE_INFO;                                                                  \
-        }                                                                                      \
+#define IMPL_PRIMITIVE_TYPE_INFO(ty, mun_primitive_type)                          \
+    template <>                                                                   \
+    struct StaticTypeInfo<ty> {                                                   \
+        static const Type& type_info() {                                          \
+            static Type TYPE_INFO = Type(mun_type_primitive(mun_primitive_type)); \
+            return TYPE_INFO;                                                     \
+        }                                                                         \
     };
 
-IMPL_PRIMITIVE_TYPE_INFO(bool, MunPrimitiveType_Bool);
-IMPL_PRIMITIVE_TYPE_INFO(float, MunPrimitiveType_F32);
-IMPL_PRIMITIVE_TYPE_INFO(double, MunPrimitiveType_F64);
-IMPL_PRIMITIVE_TYPE_INFO(int8_t, MunPrimitiveType_I8);
-IMPL_PRIMITIVE_TYPE_INFO(int16_t, MunPrimitiveType_I16);
-IMPL_PRIMITIVE_TYPE_INFO(int32_t, MunPrimitiveType_I32);
-IMPL_PRIMITIVE_TYPE_INFO(int64_t, MunPrimitiveType_I64);
+IMPL_PRIMITIVE_TYPE_INFO(bool, MUN_PRIMITIVE_TYPE_BOOL);
+IMPL_PRIMITIVE_TYPE_INFO(float, MUN_PRIMITIVE_TYPE_F32);
+IMPL_PRIMITIVE_TYPE_INFO(double, MUN_PRIMITIVE_TYPE_F64);
+IMPL_PRIMITIVE_TYPE_INFO(int8_t, MUN_PRIMITIVE_TYPE_I8);
+IMPL_PRIMITIVE_TYPE_INFO(int16_t, MUN_PRIMITIVE_TYPE_I16);
+IMPL_PRIMITIVE_TYPE_INFO(int32_t, MUN_PRIMITIVE_TYPE_I32);
+IMPL_PRIMITIVE_TYPE_INFO(int64_t, MUN_PRIMITIVE_TYPE_I64);
 // IMPL_PRIMITIVE_TYPE_REFLECTION(int128_t, MunPrimitiveType_I128);
-IMPL_PRIMITIVE_TYPE_INFO(uint8_t, MunPrimitiveType_U8);
-IMPL_PRIMITIVE_TYPE_INFO(uint16_t, MunPrimitiveType_U16);
-IMPL_PRIMITIVE_TYPE_INFO(uint32_t, MunPrimitiveType_U32);
-IMPL_PRIMITIVE_TYPE_INFO(uint64_t, MunPrimitiveType_U64);
+IMPL_PRIMITIVE_TYPE_INFO(uint8_t, MUN_PRIMITIVE_TYPE_U8);
+IMPL_PRIMITIVE_TYPE_INFO(uint16_t, MUN_PRIMITIVE_TYPE_U16);
+IMPL_PRIMITIVE_TYPE_INFO(uint32_t, MUN_PRIMITIVE_TYPE_U32);
+IMPL_PRIMITIVE_TYPE_INFO(uint64_t, MUN_PRIMITIVE_TYPE_U64);
 // IMPL_PRIMITIVE_TYPE_REFLECTION(uint128_t, MunPrimitiveType_U128);
-IMPL_PRIMITIVE_TYPE_INFO(void, MunPrimitiveType_Void);
-IMPL_PRIMITIVE_TYPE_INFO(std::tuple<>, MunPrimitiveType_Empty);
+IMPL_PRIMITIVE_TYPE_INFO(void, MUN_PRIMITIVE_TYPE_VOID);
+IMPL_PRIMITIVE_TYPE_INFO(std::tuple<>, MUN_PRIMITIVE_TYPE_EMPTY);
 
 }  // namespace mun
 
diff --git a/cpp/include/mun/struct_info.h b/cpp/include/mun/struct_info.h
deleted file mode 100644
index 808c317ef..000000000
--- a/cpp/include/mun/struct_info.h
+++ /dev/null
@@ -1,66 +0,0 @@
-#ifndef MUN_STRUCT_INFO_H
-#define MUN_STRUCT_INFO_H
-
-#include <cassert>
-
-#include "mun/field_info.h"
-#include "mun/runtime_capi.h"
-
-namespace mun {
-/**
- * @brief A wrapper around a Mun struct information handle.
- */
-class StructInfo : public TypeInfo {
-    /**
-     * @brief Constructs struct information from an instantiated `MunStructInfoHandle`.
-     */
-    StructInfo(MunTypeInfoHandle type_handle, MunStructInfoHandle struct_handle) noexcept
-        : TypeInfo(type_handle), m_struct_handle(struct_handle) {}
-
-public:
-    /**
-     * @brief Tries to cast the specified `TypeInfo` into a `StructInfo`.
-     * Returns `std::nullopt` if the `TypeInfo` does not represent a struct.
-     * @param ty The `TypeInfo` to cast
-     * @return An optional StructInfo.
-     */
-    static std::optional<StructInfo> try_cast(const TypeInfo& ty) {
-        MunTypeInfoData data;
-        auto type_info_handle = ty.handle();
-        const auto error_handle = mun_type_info_data(type_info_handle, &data);
-        assert(error_handle._0 == nullptr);
-        if (data.tag != MunTypeInfoData_Struct) {
-            return std::nullopt;
-        }
-        mun_type_info_increment_strong_count(type_info_handle);
-        return StructInfo(type_info_handle, data.struct_);
-    }
-
-    /**
-     * @brief Returns the struct's fields.
-     */
-    FieldInfoSpan fields() const noexcept {
-        MunFieldInfoSpan span;
-        const auto error_handle = mun_struct_info_fields(m_struct_handle, &span);
-        assert(error_handle._0 == nullptr);
-
-        return FieldInfoSpan(span);
-    }
-
-    /**
-     * @brief Returns the struct's memory kind.
-     */
-    MunStructMemoryKind memory_kind() const noexcept {
-        MunStructMemoryKind memory_kind;
-        const auto error_handle = mun_struct_info_memory_kind(m_struct_handle, &memory_kind);
-        assert(error_handle._0 == nullptr);
-
-        return memory_kind;
-    }
-
-private:
-    MunStructInfoHandle m_struct_handle;
-};
-}  // namespace mun
-
-#endif  // MUN_STRUCT_INFO_H
diff --git a/cpp/include/mun/struct_ref.h b/cpp/include/mun/struct_ref.h
index 4b860f583..e49fcfa6b 100644
--- a/cpp/include/mun/struct_ref.h
+++ b/cpp/include/mun/struct_ref.h
@@ -12,32 +12,11 @@
 #include "mun/marshal.h"
 #include "mun/runtime.h"
 #include "mun/static_type_info.h"
-#include "mun/struct_info.h"
-#include "mun/type_info.h"
+#include "mun/struct_type.h"
+#include "mun/type.h"
 
 namespace mun {
 namespace details {
-inline std::optional<FieldInfo> find_field(std::string_view type_name,
-                                           MunStructInfoHandle struct_info,
-                                           std::string_view field_name) noexcept {
-    MunFieldInfoSpan fields;
-    auto err = mun_struct_info_fields(struct_info, &fields);
-    assert(err._0 == nullptr);
-
-    const auto it = std::find_if(fields.data, fields.data + fields.len,
-                                 [field_name](const MunFieldInfoHandle& field_handle) {
-                                     return field_name == mun_field_info_name(field_handle);
-                                 });
-
-    if (it == fields.data + fields.len) {
-        std::cerr << "StructRef `" << type_name << "` does not contain field `" << field_name
-                  << "`." << std::endl;
-        return std::nullopt;
-    }
-
-    return std::make_optional(FieldInfo(*it));
-}
-
 inline std::string format_struct_field(std::string_view struct_name,
                                        std::string_view field_name) noexcept {
     std::string formatted;
@@ -70,11 +49,11 @@ class StructRef {
 
     StructRef& operator=(StructRef&&) noexcept = default;
 
-    /** Retrieves the raw garbage collection handle of the struct.
+    /** Retrieves the raw garbage collection type_handle of the struct.
      *
-     * \return a raw garbage collection handle
+     * \return a raw garbage collection type_handle
      */
-    MunGcPtr raw() const noexcept { return m_handle.handle(); }
+    [[nodiscard]] constexpr MunGcPtr raw() const noexcept { return m_handle.handle(); }
 
     /** Retrieves the type information of the struct.
      *
@@ -83,7 +62,10 @@ class StructRef {
      *
      * \return a pointer to the struct's type information
      */
-    TypeInfo info() const noexcept { return m_runtime->ptr_type(raw()); }
+    [[nodiscard]] StructType type() const noexcept {
+        // Safety: this is safe because a StructRef must always contain a struct type
+        return StructType::try_cast(m_runtime->ptr_type(raw())).value();
+    }
 
     /** Tries to retrieve the copied value of the field corresponding to
      * `field_name`.
@@ -122,25 +104,21 @@ template <>
 struct Marshal<StructRef> {
     using type = MunGcPtr;
 
-    static StructRef from(type ptr, const Runtime& runtime) noexcept {
-        return StructRef(runtime, ptr);
-    }
+    static StructRef from(type ptr, const Runtime& runtime) noexcept { return {runtime, ptr}; }
 
     static type to(StructRef value) noexcept { return value.raw(); }
 
     static StructRef copy_from(const type* ptr, const Runtime& runtime,
-                               const TypeInfo& type_info) noexcept {
-        MunTypeInfoData type_data;
-        auto err = mun_type_info_data(type_info.handle(), &type_data);
-        assert(err._0 == nullptr);
+                               const Type& type_info) noexcept {
+        MunTypeKind type_kind;
+        MUN_ASSERT(mun_type_kind(type_info.type_handle(), &type_kind));
 
-        // Safety: `mun_type_info_data` is guaranteed to return a value for `StructRef`s.
+        // Safety: `mun_type_kind` is guaranteed to return a value for `StructRef`s.
         MunStructMemoryKind memory_kind;
-        err = mun_struct_info_memory_kind(type_data.struct_, &memory_kind);
-        assert(err._0 == nullptr);
+        MUN_ASSERT(mun_struct_type_memory_kind(type_kind.struct_, &memory_kind));
 
         MunGcPtr gc_handle;
-        if (memory_kind == MunStructMemoryKind::Value) {
+        if (memory_kind == MUN_STRUCT_MEMORY_KIND_VALUE) {
             // Create a new managed object
             gc_handle = *runtime.gc_alloc(type_info);
 
@@ -151,20 +129,18 @@ struct Marshal<StructRef> {
             gc_handle = *ptr;
         }
 
-        return StructRef(runtime, gc_handle);
+        return {runtime, gc_handle};
     }
 
-    static void move_to(type value, type* ptr, const TypeInfo& type_info) noexcept {
-        MunTypeInfoData type_data;
-        auto err = mun_type_info_data(type_info.handle(), &type_data);
-        assert(err._0 == nullptr);
+    static void move_to(type value, type* ptr, const Type& type_info) noexcept {
+        MunTypeKind type_kind;
+        MUN_ASSERT(mun_type_kind(type_info.type_handle(), &type_kind));
 
-        // Safety: `mun_type_info_data` is guaranteed to return a value for `StructRef`s.
+        // Safety: `mun_type_kind` is guaranteed to return a value for `StructRef`s.
         MunStructMemoryKind memory_kind;
-        err = mun_struct_info_memory_kind(type_data.struct_, &memory_kind);
-        assert(err._0 == nullptr);
+        MUN_ASSERT(mun_struct_type_memory_kind(type_kind.struct_, &memory_kind));
 
-        if (memory_kind == MunStructMemoryKind::Value) {
+        if (memory_kind == MUN_STRUCT_MEMORY_KIND_VALUE) {
             // Copy the `struct(value)` into the old object
             std::memcpy(ptr, *value, type_info.size());
         } else {
@@ -173,18 +149,16 @@ struct Marshal<StructRef> {
     }
 
     static StructRef swap_at(type value, type* ptr, const Runtime& runtime,
-                             const TypeInfo& type_info) noexcept {
-        MunTypeInfoData type_data;
-        auto err = mun_type_info_data(type_info.handle(), &type_data);
-        assert(err._0 == nullptr);
+                             const Type& type_info) noexcept {
+        MunTypeKind type_kind;
+        MUN_ASSERT(mun_type_kind(type_info.type_handle(), &type_kind));
 
-        // Safety: `mun_type_info_data` is guaranteed to return a value for `StructRef`s.
+        // Safety: `mun_type_kind` is guaranteed to return a value for `StructRef`s.
         MunStructMemoryKind memory_kind;
-        err = mun_struct_info_memory_kind(type_data.struct_, &memory_kind);
-        assert(err._0 == nullptr);
+        MUN_ASSERT(mun_struct_type_memory_kind(type_kind.struct_, &memory_kind));
 
         MunGcPtr gc_handle;
-        if (memory_kind == MunStructMemoryKind::Value) {
+        if (memory_kind == MUN_STRUCT_MEMORY_KIND_VALUE) {
             // Create a new managed object
             gc_handle = *runtime.gc_alloc(type_info);
 
@@ -199,7 +173,7 @@ struct Marshal<StructRef> {
             gc_handle = *ptr;
         }
 
-        return StructRef(runtime, gc_handle);
+        return {runtime, gc_handle};
     }
 };
 }  // namespace mun
@@ -210,12 +184,12 @@ namespace mun {
 
 template <>
 struct ArgumentReflection<StructRef> {
-    static TypeInfo type_info(const StructRef& ref) noexcept { return ref.info(); }
+    static Type type_info(const StructRef& ref) noexcept { return ref.type(); }
 };
 
 template <>
 struct ReturnTypeReflection<StructRef> {
-    static bool accepts_type(const TypeInfo& ty) noexcept { return ty.is_struct(); }
+    static bool accepts_type(const Type& ty) noexcept { return ty.is_struct(); }
     static std::string type_hint() {
         using namespace std::string_literals;
         return "struct"s;
@@ -224,15 +198,8 @@ struct ReturnTypeReflection<StructRef> {
 
 template <typename T>
 std::optional<T> StructRef::get(std::string_view field_name) const noexcept {
-    const auto type_info = info();
-
-    MunTypeInfoData type_data;
-    auto err = mun_type_info_data(type_info.handle(), &type_data);
-    assert(err._0 == nullptr);
-
-    // Safety: `mun_type_info_data` is guaranteed to return a value for `StructRef`s.
-    if (const auto field_info =
-            details::find_field(type_info.name(), type_data.struct_, field_name);
+    auto type_info = type();
+    if (const auto field_info = type_info.fields().find_by_name(field_name);
         field_info.has_value()) {
         if (!ReturnTypeReflection<T>::accepts_type(field_info->type())) {
             std::cerr << "Mismatched types for `"
@@ -254,15 +221,8 @@ std::optional<T> StructRef::get(std::string_view field_name) const noexcept {
 }
 template <typename T>
 std::optional<T> StructRef::replace(std::string_view field_name, T value) noexcept {
-    const auto type_info = info();
-
-    MunTypeInfoData type_data;
-    auto err = mun_type_info_data(type_info.handle(), &type_data);
-    assert(err._0 == nullptr);
-
-    // Safety: `mun_type_info_data` is guaranteed to return a value for `StructRef`s.
-    if (const auto field_info =
-            details::find_field(type_info.name(), type_data.struct_, field_name);
+    auto type_info = type();
+    if (const auto field_info = type_info.fields().find_by_name(field_name);
         field_info.has_value()) {
         if (!ReturnTypeReflection<T>::accepts_type(field_info->type())) {
             std::cerr << "Mismatched types for `"
@@ -284,15 +244,8 @@ std::optional<T> StructRef::replace(std::string_view field_name, T value) noexce
 }
 template <typename T>
 bool StructRef::set(std::string_view field_name, T value) noexcept {
-    const auto type_info = info();
-
-    MunTypeInfoData type_data;
-    auto err = mun_type_info_data(type_info.handle(), &type_data);
-    assert(err._0 == nullptr);
-
-    // Safety: `mun_type_info_data` is guaranteed to return a value for `StructRef`s.
-    if (const auto field_info =
-            details::find_field(type_info.name(), type_data.struct_, field_name);
+    auto type_info = type();
+    if (const auto field_info = type_info.fields().find_by_name(field_name);
         field_info.has_value()) {
         if (!ReturnTypeReflection<T>::accepts_type(field_info->type())) {
             std::cerr << "Mismatched types for `"
diff --git a/cpp/include/mun/struct_type.h b/cpp/include/mun/struct_type.h
new file mode 100644
index 000000000..8c338a1da
--- /dev/null
+++ b/cpp/include/mun/struct_type.h
@@ -0,0 +1,61 @@
+#ifndef MUN_STRUCT_INFO_H
+#define MUN_STRUCT_INFO_H
+
+#include <cassert>
+
+#include "mun/field_info.h"
+#include "mun/runtime_capi.h"
+
+namespace mun {
+/**
+ * @brief A wrapper around a Mun struct information handle.
+ */
+class StructType : public Type {
+    /**
+     * @brief Constructs struct information from a `MunStructInfo` and its associated Type.
+     */
+    constexpr StructType(MunType type_handle, MunStructInfo struct_info) noexcept
+        : Type(type_handle), m_struct_info(struct_info) {}
+
+public:
+    /**
+     * @brief Tries to cast the specified `Type` into a `StructType`.
+     * Returns `std::nullopt` if the `Type` does not represent a struct.
+     * \param ty The `Type` to cast
+     * \return The StructType if the cast was successful.
+     */
+    static std::optional<StructType> try_cast(Type ty) {
+        MunTypeKind kind;
+        MUN_ASSERT(mun_type_kind(ty.type_handle(), &kind));
+        if (kind.tag == MUN_TYPE_KIND_STRUCT) {
+            return std::make_optional(
+                StructType(std::move(ty).release_type_handle(), kind.struct_));
+        } else {
+            return std::nullopt;
+        }
+    }
+
+    /**
+     * @brief Returns the struct's fields.
+     */
+    StructFields fields() const noexcept {
+        MunFields fields;
+        MUN_ASSERT(mun_struct_type_fields(m_struct_info, &fields));
+        return StructFields(fields);
+    }
+
+    /**
+     * @brief Returns the struct's memory kind.
+     */
+    [[nodiscard]] MunStructMemoryKind memory_kind() const noexcept {
+        MunStructMemoryKind memory_kind;
+        MUN_ASSERT(mun_struct_type_memory_kind(m_struct_info, &memory_kind));
+        return memory_kind;
+    }
+
+private:
+    MunStructInfo m_struct_info;
+};
+}  // namespace mun
+
+#endif  // MUN_STRUCT_INFO_H
diff --git a/cpp/include/mun/type.h b/cpp/include/mun/type.h
new file mode 100644
index 000000000..57cdac55e
--- /dev/null
+++ b/cpp/include/mun/type.h
@@ -0,0 +1,224 @@
+#ifndef MUN_TYPE_INFO_H
+#define MUN_TYPE_INFO_H
+
+#include <optional>
+
+#include "mun/md5.h"
+#include "mun/runtime_capi.h"
+
+namespace mun {
+/**
+ * @brief A wrapper around a Mun type information handle.
+ *
+ * Moving of `Type` leaves the class in an valid but undefined state. Calling any of the accessors
+ * on a moved `Type` will result in an assertion.
+ */
+class Type {
+public:
+    /**
+     * @brief Constructs type information from an instantiated `MunType`.
+     *
+     * This function assumes it is granted ownership of `type_handle`.
+     *
+     * \param handle A C-style Mun type type_handle.
+     */
+    constexpr explicit Type(MunType handle) noexcept : m_handle(handle) {}
+
+    ~Type() noexcept {
+        if (m_handle._0 != nullptr) {
+            MUN_ASSERT(mun_type_release(m_handle));
+            m_handle._0 = nullptr;
+        }
+    }
+
+    Type(const Type& other) noexcept : m_handle(other.m_handle) {
+        MUN_ASSERT(mun_type_add_reference(m_handle));
+    }
+
+    constexpr Type(Type&& other) noexcept : m_handle(other.m_handle) {
+        other.m_handle._0 = nullptr;
+    }
+
+    Type& operator=(const Type& other) {
+        if (other.m_handle._0 != nullptr) {
+            MUN_ASSERT(mun_type_add_reference(other.m_handle));
+        }
+        if (m_handle._0 != nullptr) {
+            MUN_ASSERT(mun_type_release(m_handle));
+        }
+        m_handle = other.m_handle;
+    }
+
+    constexpr Type& operator=(Type&& other) noexcept {
+        m_handle = other.m_handle;
+        other.m_handle._0 = nullptr;
+        return *this;
+    }
+
+    bool operator==(const Type& other) const { return mun_type_equal(m_handle, other.m_handle); }
+
+    /**
+     * Returns true if this TypeInfo represents a struct.
+     */
+    [[nodiscard]] bool is_struct() const noexcept {
+        MunTypeKind type_kind;
+        MUN_ASSERT(mun_type_kind(m_handle, &type_kind));
+        return type_kind.tag == MUN_TYPE_KIND_STRUCT;
+    }
+
+    /**
+     * Returns true if this TypeInfo represents a pointer.
+     */
+    [[nodiscard]] bool is_pointer() const noexcept {
+        MunTypeKind type_kind;
+        MUN_ASSERT(mun_type_kind(m_handle, &type_kind));
+        return type_kind.tag == MUN_TYPE_KIND_POINTER;
+    }
+
+    /**
+     * Returns true if this TypeInfo represents a primitive.
+     */
+    [[nodiscard]] bool is_primitive() const noexcept {
+        MunTypeKind type_kind;
+        MUN_ASSERT(mun_type_kind(m_handle, &type_kind));
+        return type_kind.tag == MUN_TYPE_KIND_PRIMITIVE;
+    }
+
+    /**
+     * @brief Retrieves the type's name.
+     */
+    [[nodiscard]] std::string name() const noexcept {
+        const char* name;
+        MUN_ASSERT(mun_type_name(m_handle, &name));
+        std::string str(name);
+        mun_string_destroy(name);
+        return str;
+    }
+
+    /**
+     * @brief Retrieves the type's size in bytes.
+     */
+    [[nodiscard]] size_t size() const noexcept {
+        size_t size;
+        MUN_ASSERT(mun_type_size(m_handle, &size));
+        return size;
+    }
+
+    /**
+     * @brief Retrieves the type's alignment in bytes.
+     */
+    [[nodiscard]] size_t alignment() const noexcept {
+        size_t alignment;
+        MUN_ASSERT(mun_type_alignment(m_handle, &alignment));
+        return alignment;
+    }
+
+    /**
+     * @brief Returns the wrapped C type handle.
+     *
+     * Ownership of the type_handle remains with this instance, it is not transferred. See
+     * [`Type::release_type_handle`] to transfer ownership of the handle.
+     */
+    [[nodiscard]] inline constexpr const MunType& type_handle() const noexcept { return m_handle; }
+
+    /**
+     * @brief Returns the wrapped C type handle, transferring ownership.
+     */
+    [[nodiscard]] inline constexpr MunType release_type_handle() && noexcept {
+        auto handle = m_handle;
+        m_handle._0 = nullptr;
+        return handle;
+    }
+
+private:
+    MunType m_handle;
+};
+
+/**
+ * @brief A wrapper around a span of Mun types.
+ *
+ * The array is owned by this instance.
+ */
+class TypeArray {
+public:
+    struct Iterator {
+        using iterator_category = std::random_access_iterator_tag;
+        using difference_type = std::ptrdiff_t;
+        using value_type = Type;
+
+        constexpr explicit Iterator(MunType const* ptr) : m_ptr(ptr){};
+
+        value_type operator*() const {
+            // MunTypes owns the Types inside it, mun::Type assumes it takes ownership of the data,
+            // therefor we increase reference count here.
+            MUN_ASSERT(mun_type_add_reference(*m_ptr));
+            return Type(*m_ptr);
+        }
+
+        Iterator& operator++() {
+            m_ptr++;
+            return *this;
+        }
+
+        Iterator operator++(int) {
+            Iterator tmp = *this;
+            ++(*this);
+            return tmp;
+        }
+
+        friend bool operator==(const Iterator& a, const Iterator& b) { return a.m_ptr == b.m_ptr; };
+        friend bool operator!=(const Iterator& a, const Iterator& b) { return a.m_ptr != b.m_ptr; };
+
+    private:
+        MunType const* m_ptr;
+    };
+
+    /**
+     * @brief Constructs a type information span from an instantiated `MunTypeInfoSpan`.
+     */
+    constexpr explicit TypeArray(MunTypes types) noexcept : m_data(types) {}
+
+    ~TypeArray() noexcept {
+        if (m_data.types != nullptr) {
+            mun_types_destroy(m_data);
+            m_data.types = nullptr;
+        }
+    }
+
+    TypeArray(const TypeArray&) = delete;
+    TypeArray& operator=(const TypeArray&) = delete;
+
+    constexpr TypeArray(TypeArray&& other) noexcept : m_data(other.m_data) {
+        other.m_data.types = nullptr;
+    };
+
+    TypeArray& operator=(TypeArray&& other) noexcept {
+        std::swap(m_data, other.m_data);
+        return *this;
+    };
+
+    /**
+     * @brief Returns an iterator to the beginning.
+     */
+    [[nodiscard]] inline constexpr Iterator begin() const noexcept {
+        return Iterator(m_data.types);
+    }
+
+    /**
+     * @brief Returns an iterator to the end.
+     */
+    [[nodiscard]] inline constexpr Iterator end() const noexcept {
+        return Iterator(m_data.types + m_data.count);
+    }
+
+    /**
+     * @brief Returns the number of elements in the array.
+     */
+    [[nodiscard]] inline constexpr size_t size() const noexcept { return m_data.count; }
+
+private:
+    MunTypes m_data;
+};
+}  // namespace mun
+
+#endif  // MUN_TYPE_INFO_H
diff --git a/cpp/include/mun/type_info.h b/cpp/include/mun/type_info.h
deleted file mode 100644
index 41d2fc3ad..000000000
--- a/cpp/include/mun/type_info.h
+++ /dev/null
@@ -1,162 +0,0 @@
-#ifndef MUN_TYPE_INFO_H
-#define MUN_TYPE_INFO_H
-
-#include <optional>
-
-#include "mun/md5.h"
-#include "mun/runtime_capi.h"
-
-namespace mun {
-/**
- * @brief A wrapper around a Mun type information handle.
- */
-class TypeInfo {
-    friend class StructInfo;
-
-public:
-    /**
-     * @brief Constructs type information from an instantiated `MunTypeInfoHandle`.
-     */
-    TypeInfo(MunTypeInfoHandle handle) noexcept : m_handle(handle) {}
-
-    ~TypeInfo() noexcept {
-        if (m_handle._0 != nullptr) {
-            mun_type_info_decrement_strong_count(m_handle);
-            m_handle._0 = nullptr;
-        }
-    }
-
-    TypeInfo(const TypeInfo& other) noexcept : m_handle(other.m_handle) {
-        mun_type_info_increment_strong_count(m_handle);
-    }
-
-    TypeInfo(TypeInfo&& other) noexcept : m_handle(other.m_handle) { other.m_handle._0 = nullptr; }
-
-    TypeInfo& operator=(const TypeInfo& other) {
-        m_handle = other.m_handle;
-        mun_type_info_increment_strong_count(m_handle);
-        return *this;
-    }
-
-    TypeInfo& operator=(TypeInfo&& other) noexcept {
-        m_handle = other.m_handle;
-        other.m_handle._0 = nullptr;
-        return *this;
-    }
-
-    bool operator==(const TypeInfo& other) const {
-        return mun_type_info_eq(m_handle, other.m_handle);
-    }
-
-    /**
-     * Returns true if this TypeInfo represents a struct.
-     */
-    [[nodiscard]] bool is_struct() const noexcept {
-        MunTypeInfoData data;
-        const auto error_handle = mun_type_info_data(m_handle, &data);
-        assert(error_handle._0 == nullptr);
-        return data.tag == MunTypeInfoData_Struct;
-    }
-
-    /**
-     * Returns true if this TypeInfo represents a pointer.
-     */
-    [[nodiscard]] bool is_pointer() const noexcept {
-        MunTypeInfoData data;
-        const auto error_handle = mun_type_info_data(m_handle, &data);
-        assert(error_handle._0 == nullptr);
-        return data.tag == MunTypeInfoData_Pointer;
-    }
-
-    /**
-     * Returns true if this TypeInfo represents a primitive.
-     */
-    [[nodiscard]] bool is_primitive() const noexcept {
-        MunTypeInfoData data;
-        const auto error_handle = mun_type_info_data(m_handle, &data);
-        assert(error_handle._0 == nullptr);
-        return data.tag == MunTypeInfoData_Primitive;
-    }
-
-    /**
-     * @brief Retrieves the type's name.
-     */
-    [[nodiscard]] std::string_view name() const noexcept {
-        const auto ptr = mun_type_info_name(m_handle);
-        assert(ptr);
-        return ptr;
-    }
-
-    /**
-     * @brief Retrieves the type's size in bytes.
-     */
-    [[nodiscard]] size_t size() const noexcept {
-        size_t size;
-        const auto error_handle = mun_type_info_size(m_handle, &size);
-        assert(error_handle._0 == nullptr);
-        return size;
-    }
-
-    /**
-     * @brief Retrieves the type's alignment.
-     */
-    [[nodiscard]] size_t alignment() const noexcept {
-        size_t align;
-        const auto error_handle = mun_type_info_align(m_handle, &align);
-        assert(error_handle._0 == nullptr);
-        return align;
-    }
-
-    /**
-     * @brief Returns the type's handle.
-     */
-    [[nodiscard]] MunTypeInfoHandle handle() const noexcept { return m_handle; }
-
-private:
-    MunTypeInfoHandle m_handle;
-};
-
-/**
- * @brief A wrapper around a span of Mun type informations, which are owned by the Mun runtime.
- */
-class TypeInfoSpan {
-public:
-    /**
-     * @brief Constructs a type information span from an instantiated `MunTypeInfoSpan`.
-     */
-    TypeInfoSpan(MunTypeInfoSpan span) noexcept : m_span(span) {}
-
-    ~TypeInfoSpan() noexcept { mun_type_info_span_destroy(m_span); }
-
-    TypeInfoSpan(const TypeInfoSpan&) = delete;
-    TypeInfoSpan& operator=(const TypeInfoSpan&) = delete;
-
-    TypeInfoSpan(TypeInfoSpan&&) = default;
-    TypeInfoSpan& operator=(TypeInfoSpan&&) = default;
-
-    /**
-     * @brief Returns an iterator to the beginning.
-     */
-    const MunTypeInfoHandle* begin() const noexcept { return data(); }
-
-    /**
-     * @brief Returns an iterator to the end.
-     */
-    const MunTypeInfoHandle* end() const noexcept { return data() + size() + 1; }
-
-    /**
-     * @brief Returns a pointer to the beginning of the sequence of elements.
-     */
-    const MunTypeInfoHandle* data() const noexcept { return m_span.data; }
-
-    /**
-     * @brief Returns the number of elements in the sequence.
-     */
-    size_t size() const noexcept { return m_span.len; }
-
-private:
-    MunTypeInfoSpan m_span;
-};
-}  // namespace mun
-
-#endif  // MUN_TYPE_INFO_H
diff --git a/crates/mun/src/ops/start.rs b/crates/mun/src/ops/start.rs
index 5b9dd7f52..197f35d31 100644
--- a/crates/mun/src/ops/start.rs
+++ b/crates/mun/src/ops/start.rs
@@ -58,7 +58,7 @@ pub fn start(args: Args) -> anyhow::Result<ExitStatus> {
     } else {
         return Err(anyhow!(
             "Only native Mun return types are supported for entry points. Found: {}",
-            return_type.name
+            return_type.name()
         ));
     };
     Ok(ExitStatus::Success)
diff --git a/crates/mun_abi/src/type_info.rs b/crates/mun_abi/src/type_info.rs
index d084e8bd8..816e71bde 100644
--- a/crates/mun_abi/src/type_info.rs
+++ b/crates/mun_abi/src/type_info.rs
@@ -85,9 +85,9 @@ impl<'a> TypeDefinition<'a> {
     }
 
     /// Returns the GUID if this type represents a concrete type.
-    pub fn as_concrete(&self) -> Option<&Guid> {
+    pub fn as_concrete(&self) -> &Guid {
         match &self.data {
-            TypeDefinitionData::Struct(s) => Some(&s.guid),
+            TypeDefinitionData::Struct(s) => &s.guid,
         }
     }
 
diff --git a/crates/mun_capi_utils/Cargo.toml b/crates/mun_capi_utils/Cargo.toml
new file mode 100644
index 000000000..1163ad24d
--- /dev/null
+++ b/crates/mun_capi_utils/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "mun_capi_utils"
+version = "0.1.0"
+edition = "2021"
+authors = ["The Mun Team <team@mun-lang.org>"]
+description = "Common functionality between C api crates."
+documentation = "https://docs.mun-lang.org/v0.3"
+readme = "README.md"
+homepage = "https://mun-lang.org"
+repository = "https://github.com/mun-lang/mun"
+license = "MIT OR Apache-2.0"
+keywords = ["game", "hot-reloading", "language", "mun", "scripting"]
+categories = ["game-development", "mun"]
+
+[dependencies]
+insta = { version ="1.12.0", features=["ron"], optional=true}
diff --git a/crates/mun_capi_utils/src/error.rs b/crates/mun_capi_utils/src/error.rs
new file mode 100644
index 000000000..dc5a804c8
--- /dev/null
+++ b/crates/mun_capi_utils/src/error.rs
@@ -0,0 +1,144 @@
+//! Exposes error reporting using the C ABI.
+
+use std::{
+    ffi::{CStr, CString},
+    os::raw::c_char,
+    ptr,
+};
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+/// A C-style handle to an error message.
+///
+/// If the handle contains a non-null pointer, an error occurred.
+/// cbindgen:field-names=[error_string]
+pub struct ErrorHandle(pub *const c_char);
+
+impl ErrorHandle {
+    /// Constructs an `ErrorHandle` from the specified error message.
+    pub fn new<T: Into<Vec<u8>>>(error_message: T) -> Self {
+        let error_message = CString::new(error_message).expect("Invalid error message");
+        Self(CString::into_raw(error_message))
+    }
+
+    /// Returns true if this error handle doesnt actually contain any error.
+    pub fn is_ok(&self) -> bool {
+        self.0.is_null()
+    }
+
+    /// Returns true if this error handle contains an error
+    pub fn is_err(&self) -> bool {
+        !self.0.is_null()
+    }
+
+    /// Returns the error associated with this instance or `None` if there is no error.
+    ///
+    /// # Safety
+    ///
+    /// If the error contained in this handle has previously been deallocated the data may have been
+    /// corrupted.
+    pub unsafe fn err(&self) -> Option<&CStr> {
+        if self.is_err() {
+            Some(CStr::from_ptr(self.0))
+        } else {
+            None
+        }
+    }
+}
+
+impl Default for ErrorHandle {
+    fn default() -> Self {
+        Self(ptr::null())
+    }
+}
+
+impl<T: Into<Vec<u8>>> From<T> for ErrorHandle {
+    fn from(bytes: T) -> Self {
+        ErrorHandle::new(bytes)
+    }
+}
+
+/// Destructs the error message corresponding to the specified handle.
+///
+/// # Safety
+///
+/// Only call this function on an ErrorHandle once.
+#[no_mangle]
+pub unsafe extern "C" fn mun_error_destroy(error: ErrorHandle) {
+    if !error.0.is_null() {
+        let _drop = CString::from_raw(error.0 as *mut c_char);
+    }
+}
+
+#[macro_export]
+macro_rules! mun_error_try {
+    ($expr:expr $(,)?) => {
+        match $expr {
+            Ok(val) => val,
+            Err(err) => {
+                return ErrorHandle::from(err);
+            }
+        }
+    };
+}
+
+#[macro_export]
+macro_rules! try_deref_mut {
+    ($expr:expr $(,)?) => {
+        match ($expr).as_mut() {
+            Some(val) => val,
+            None => {
+                return ErrorHandle::new(concat!(
+                    "invalid argument '",
+                    stringify!($expr),
+                    "': null pointer"
+                ));
+            }
+        }
+    };
+}
+
+#[macro_export]
+macro_rules! try_deref {
+    ($expr:expr $(,)?) => {
+        match ($expr).as_ref() {
+            Some(val) => val,
+            None => {
+                return ErrorHandle::new(concat!(
+                    "invalid argument '",
+                    stringify!($expr),
+                    "': null pointer"
+                ));
+            }
+        }
+    };
+}
+
+#[macro_export]
+macro_rules! assert_error {
+    ($expr:expr $(,)?) => {
+        let err = $expr;
+        assert!(err.is_err());
+        unsafe { $crate::mun_string_destroy(err.0) };
+    };
+}
+
+#[cfg(feature = "insta")]
+#[macro_export]
+macro_rules! assert_error_snapshot {
+    ($expr:expr, @$snapshot:literal) => {
+        let error_handle = $expr;
+        let expr = match unsafe {error_handle.err() } {
+            Some(err) => {
+                let err_str: String = err.to_str().unwrap().to_owned();
+                unsafe { $crate::mun_string_destroy(error_handle.0) };
+                err_str
+            },
+            None => panic!("expected an error")
+        };
+        let name = stringify!($expr);
+        insta::assert_ron_snapshot!(
+            expr, @$snapshot
+        );
+    };
+}
diff --git a/crates/mun_capi_utils/src/lib.rs b/crates/mun_capi_utils/src/lib.rs
new file mode 100644
index 000000000..d56f692f2
--- /dev/null
+++ b/crates/mun_capi_utils/src/lib.rs
@@ -0,0 +1,77 @@
+use std::ffi::CStr;
+use std::{ffi::CString, os::raw::c_char};
+
+pub mod error;
+
+pub use error::ErrorHandle;
+
+/// Deallocates a string that was allocated by the runtime.
+///
+/// # Safety
+///
+/// This function receives a raw pointer as parameter. Only when the argument is not a null pointer,
+/// its content will be deallocated. Passing pointers to invalid data or memory allocated by other
+/// processes, will lead to undefined behavior.
+#[no_mangle]
+pub unsafe extern "C" fn mun_string_destroy(string: *const c_char) {
+    if !string.is_null() {
+        // Destroy the string
+        let _string = CString::from_raw(string as *mut _);
+    }
+}
+
+/// Tries to convert a C style string pointer to a CStr.
+///
+/// # Safety
+///
+/// The caller must provide a valid C string with a null terminator, whose content doesnt change
+/// during the lifetime `'a`.
+pub unsafe fn try_convert_c_string<'a>(string: *const c_char) -> Result<&'a str, &'static str> {
+    if string.is_null() {
+        return Err("null pointer");
+    }
+
+    match CStr::from_ptr::<'a>(string).to_str() {
+        Ok(name) => Ok(name),
+        Err(_) => Err("invalid UTF-8 encoded"),
+    }
+}
+
+#[macro_export]
+macro_rules! assert_getter1 {
+    ($fun:ident ( $handle:expr, $($arg:ident),+ $(,)?)) => {
+        $(
+            let mut $arg = std::mem::MaybeUninit::uninit();
+        )+
+        assert!(unsafe { $fun($handle, $($arg.as_mut_ptr()),+) }.is_ok());
+        $(
+            let $arg = unsafe { $arg.assume_init() };
+        )+
+    };
+}
+
+#[macro_export]
+macro_rules! assert_getter2 {
+    ($fun:ident ( $handle:expr, $arg1:expr, $($arg:ident),+ $(,)?)) => {
+        $(
+            let mut $arg = std::mem::MaybeUninit::uninit();
+        )+
+        assert!(unsafe { $fun($handle, $arg1, $($arg.as_mut_ptr()),+) }.is_ok());
+        $(
+            let $arg = unsafe { $arg.assume_init() };
+        )+
+    };
+}
+
+#[macro_export]
+macro_rules! assert_getter3 {
+    ($fun:ident ( $handle:expr, $arg1:expr, $arg2:expr, $($arg:ident),+ $(,)?)) => {
+        $(
+            let mut $arg = std::mem::MaybeUninit::uninit();
+        )+
+        assert!(unsafe { $fun($handle, $arg1, $arg2, $($arg.as_mut_ptr()),+) }.is_ok());
+        $(
+            let $arg = unsafe { $arg.assume_init() };
+        )+
+    };
+}
diff --git a/crates/mun_memory/Cargo.toml b/crates/mun_memory/Cargo.toml
index 981cc0c09..36391f43d 100644
--- a/crates/mun_memory/Cargo.toml
+++ b/crates/mun_memory/Cargo.toml
@@ -20,6 +20,9 @@ once_cell = "1.4.0"
 parking_lot = "0.12.0"
 rustc-hash = "1.1"
 thiserror = "1.0.19"
+capi_utils = { version = "=0.1.0", path = "../mun_capi_utils", package= "mun_capi_utils" }
 
 [dev-dependencies]
 paste = "1.0"
+capi_utils = { version = "=0.1.0", path = "../mun_capi_utils", package= "mun_capi_utils", features = ["insta"] }
+insta = { version = "1.12.0", features = ["ron"]}
diff --git a/crates/mun_memory/src/cast.rs b/crates/mun_memory/src/cast.rs
index de59e59ac..b5770396f 100644
--- a/crates/mun_memory/src/cast.rs
+++ b/crates/mun_memory/src/cast.rs
@@ -1,8 +1,8 @@
 #![allow(clippy::mutable_key_type)]
 
-use crate::{HasStaticTypeInfo, TypeInfo};
+use crate::{HasStaticType, Type};
 use lazy_static::lazy_static;
-use std::{collections::HashMap, ptr::NonNull, sync::Arc};
+use std::{collections::HashMap, ptr::NonNull};
 
 type CastFn = fn(NonNull<u8>, NonNull<u8>);
 
@@ -16,7 +16,7 @@ macro_rules! insert_cast_fn {
 }
 
 lazy_static! {
-    static ref CAST_FN_TABLE: HashMap<(Arc<TypeInfo>, Arc<TypeInfo>), CastFn> = {
+    static ref CAST_FN_TABLE: HashMap<(Type, Type), CastFn> = {
         let mut table = HashMap::new();
         insert_cast_fn!(table, f32, f64);
         insert_cast_fn!(table, i8, i16);
@@ -61,12 +61,7 @@ where
     unsafe { *dest.cast::<B>().as_mut() = value.into() };
 }
 
-pub fn try_cast_from_to(
-    old_id: Arc<TypeInfo>,
-    new_id: Arc<TypeInfo>,
-    src: NonNull<u8>,
-    dest: NonNull<u8>,
-) -> bool {
+pub fn try_cast_from_to(old_id: Type, new_id: Type, src: NonNull<u8>, dest: NonNull<u8>) -> bool {
     if let Some(cast_fn) = CAST_FN_TABLE.get(&(old_id, new_id)) {
         cast_fn(src, dest);
         true
@@ -78,13 +73,13 @@ pub fn try_cast_from_to(
 #[cfg(test)]
 mod tests {
     use super::try_cast_from_to;
-    use crate::HasStaticTypeInfo;
+    use crate::HasStaticType;
     use std::ptr::NonNull;
 
     fn assert_cast<A, B>(a: A, mut b: B)
     where
-        A: Copy + Into<B> + HasStaticTypeInfo,
-        B: PartialEq + std::fmt::Debug + HasStaticTypeInfo,
+        A: Copy + Into<B> + HasStaticType,
+        B: PartialEq + std::fmt::Debug + HasStaticType,
     {
         assert!(try_cast_from_to(
             A::type_info().clone(),
diff --git a/crates/mun_memory/src/diff.rs b/crates/mun_memory/src/diff.rs
index f762e72d8..df609030a 100644
--- a/crates/mun_memory/src/diff.rs
+++ b/crates/mun_memory/src/diff.rs
@@ -1,11 +1,6 @@
 pub mod myers;
 
-use std::sync::Arc;
-
-use crate::{
-    type_info::{FieldInfo, TypeInfo},
-    TypeFields,
-};
+use crate::{r#type::Field, r#type::Type};
 
 #[derive(Clone, Debug, Eq, PartialEq)]
 pub enum FieldEditKind {
@@ -77,7 +72,7 @@ impl PartialOrd for Diff {
 }
 
 /// Given an `old` and a `new` set of types, calculates the difference.
-pub fn diff(old: &[Arc<TypeInfo>], new: &[Arc<TypeInfo>]) -> Vec<Diff> {
+pub fn diff(old: &[Type], new: &[Type]) -> Vec<Diff> {
     let diff = myers::diff(old, new);
     let mut mapping: Vec<Diff> = Vec::with_capacity(diff.len());
     let (deletions, insertions) = myers::split_diff(&diff);
@@ -86,23 +81,23 @@ pub fn diff(old: &[Arc<TypeInfo>], new: &[Arc<TypeInfo>]) -> Vec<Diff> {
     // compared separately.
     let deleted_primitives = deletions
         .iter()
-        .filter(|idx| unsafe { old.get_unchecked(**idx) }.data.is_primitive())
+        .filter(|idx| unsafe { old.get_unchecked(**idx) }.is_primitive())
         .cloned()
         .collect();
     let deleted_structs = deletions
         .iter()
-        .filter(|idx| unsafe { old.get_unchecked(**idx) }.data.is_struct())
+        .filter(|idx| unsafe { old.get_unchecked(**idx) }.is_struct())
         .cloned()
         .collect();
 
     let inserted_primitives = insertions
         .iter()
-        .filter(|idx| unsafe { new.get_unchecked(**idx) }.data.is_primitive())
+        .filter(|idx| unsafe { new.get_unchecked(**idx) }.is_primitive())
         .cloned()
         .collect();
     let inserted_structs = insertions
         .iter()
-        .filter(|idx| unsafe { new.get_unchecked(**idx) }.data.is_struct())
+        .filter(|idx| unsafe { new.get_unchecked(**idx) }.is_struct())
         .cloned()
         .collect();
 
@@ -122,8 +117,8 @@ pub fn diff(old: &[Arc<TypeInfo>], new: &[Arc<TypeInfo>]) -> Vec<Diff> {
 }
 
 fn append_primitive_mapping(
-    old: &[Arc<TypeInfo>],
-    new: &[Arc<TypeInfo>],
+    old: &[Type],
+    new: &[Type],
     deletions: Vec<usize>,
     insertions: Vec<usize>,
     mapping: &mut Vec<Diff>,
@@ -164,14 +159,14 @@ fn append_primitive_mapping(
 #[derive(Eq, PartialEq)]
 struct UniqueFieldInfo<'a> {
     name: &'a str,
-    type_info: &'a TypeInfo,
+    type_info: Type,
 }
 
-impl<'a> From<&'a FieldInfo> for UniqueFieldInfo<'a> {
-    fn from(other: &'a FieldInfo) -> Self {
+impl<'a> From<Field<'a>> for UniqueFieldInfo<'a> {
+    fn from(other: Field<'a>) -> Self {
         Self {
-            name: &other.name,
-            type_info: &other.type_info,
+            name: other.name(),
+            type_info: other.ty(),
         }
     }
 }
@@ -180,20 +175,28 @@ impl<'a> From<&'a FieldInfo> for UniqueFieldInfo<'a> {
 /// for `insertions` into the `new` slice of types, appends the corresponding `Diff` mapping
 /// for all
 fn append_struct_mapping(
-    old: &[Arc<TypeInfo>],
-    new: &[Arc<TypeInfo>],
+    old: &[Type],
+    new: &[Type],
     deletions: Vec<usize>,
     insertions: Vec<usize>,
     mapping: &mut Vec<Diff>,
 ) {
     let old_fields: Vec<Vec<UniqueFieldInfo>> = old
         .iter()
-        .map(|ty| ty.fields().iter().map(UniqueFieldInfo::from).collect())
+        .map(|ty| {
+            ty.as_struct()
+                .map(|s| s.fields().iter().map(UniqueFieldInfo::from).collect())
+                .unwrap_or_else(Vec::new)
+        })
         .collect();
 
     let new_fields: Vec<Vec<UniqueFieldInfo>> = new
         .iter()
-        .map(|ty| ty.fields().iter().map(UniqueFieldInfo::from).collect())
+        .map(|ty| {
+            ty.as_struct()
+                .map(|s| s.fields().iter().map(UniqueFieldInfo::from).collect())
+                .unwrap_or_else(Vec::new)
+        })
         .collect();
 
     let num_deleted = deletions.len();
@@ -221,7 +224,7 @@ fn append_struct_mapping(
                     // let min = new_fields.len() + old_fields.len();
 
                     // If the type's name is equal
-                    if old_ty.name == new_ty.name || length == 0 {
+                    if old_ty.name() == new_ty.name() || length == 0 {
                         // TODO: Potentially we want to retain an X% for types with equal names,
                         // whilst allowing types with different names to be modified for up to Y%.
                         length
diff --git a/crates/mun_memory/src/gc.rs b/crates/mun_memory/src/gc.rs
index d12b85d24..396de3382 100644
--- a/crates/mun_memory/src/gc.rs
+++ b/crates/mun_memory/src/gc.rs
@@ -2,8 +2,8 @@ mod mark_sweep;
 mod ptr;
 mod root_ptr;
 
-use crate::type_info::TypeInfo;
-use std::{marker::PhantomData, sync::Arc};
+use crate::r#type::Type;
+use std::marker::PhantomData;
 
 pub use mark_sweep::MarkSweep;
 pub use ptr::{GcPtr, HasIndirectionPtr, RawGcPtr};
@@ -26,10 +26,10 @@ pub trait TypeTrace: Send + Sync {
 /// An object that can be used to allocate and collect memory.
 pub trait GcRuntime: Send + Sync {
     /// Allocates an object of the given type returning a GcPtr
-    fn alloc(&self, ty: &Arc<TypeInfo>) -> GcPtr;
+    fn alloc(&self, ty: &Type) -> GcPtr;
 
     /// Returns the type of the specified `obj`.
-    fn ptr_type(&self, obj: GcPtr) -> Arc<TypeInfo>;
+    fn ptr_type(&self, obj: GcPtr) -> Type;
 
     /// Roots the specified `obj`, which keeps it and objects it references alive. Objects marked
     /// as root, must call `unroot` before they can be collected. An object can be rooted multiple
diff --git a/crates/mun_memory/src/gc/mark_sweep.rs b/crates/mun_memory/src/gc/mark_sweep.rs
index 10fdde12f..0baae6e1a 100644
--- a/crates/mun_memory/src/gc/mark_sweep.rs
+++ b/crates/mun_memory/src/gc/mark_sweep.rs
@@ -2,7 +2,7 @@ use crate::{
     cast,
     gc::{Event, GcPtr, GcRuntime, HasIndirectionPtr, Observer, RawGcPtr, Stats, TypeTrace},
     mapping::{self, FieldMapping, MemoryMapper},
-    type_info::{TypeInfo, TypeInfoData},
+    r#type::Type,
 };
 use mapping::{Conversion, Mapping};
 use parking_lot::RwLock;
@@ -11,12 +11,11 @@ use std::{
     ops::Deref,
     pin::Pin,
     ptr::NonNull,
-    sync::Arc,
 };
 
 pub struct Trace {
     obj: GcPtr,
-    ty: Arc<TypeInfo>,
+    ty: Type,
     index: usize,
 }
 
@@ -24,21 +23,18 @@ impl Iterator for Trace {
     type Item = GcPtr;
 
     fn next(&mut self) -> Option<Self::Item> {
-        let struct_ty = self.ty.as_ref().as_struct()?;
-        let field_count = struct_ty.fields.len();
+        let struct_ty = self.ty.as_struct()?;
+        let fields = struct_ty.fields();
+        let field_count = fields.len();
         while self.index < field_count {
             let index = self.index;
             self.index += 1;
 
-            let field = &struct_ty.fields[index];
-            if let TypeInfoData::Struct(field_struct_ty) = &field.type_info.data {
-                if field_struct_ty.memory_kind == abi::StructMemoryKind::Gc {
+            let field = fields.get(index).unwrap();
+            if let Some(field_struct_ty) = &field.ty().as_struct() {
+                if field_struct_ty.is_gc_struct() {
                     return Some(unsafe {
-                        *self
-                            .obj
-                            .deref::<u8>()
-                            .add(usize::from(field.offset))
-                            .cast::<GcPtr>()
+                        *self.obj.deref::<u8>().add(field.offset()).cast::<GcPtr>()
                     });
                 }
             }
@@ -47,7 +43,7 @@ impl Iterator for Trace {
     }
 }
 
-impl TypeTrace for Arc<TypeInfo> {
+impl TypeTrace for Type {
     type Trace = Trace;
 
     fn trace(&self, obj: GcPtr) -> Self::Trace {
@@ -97,10 +93,10 @@ where
     }
 
     /// Logs an allocation
-    fn log_alloc(&self, handle: GcPtr, ty: &TypeInfo) {
+    fn log_alloc(&self, handle: GcPtr, ty: &Type) {
         {
             let mut stats = self.stats.write();
-            stats.allocated_memory += ty.layout.size();
+            stats.allocated_memory += ty.layout().size();
         }
 
         self.observer.event(Event::Allocation(handle));
@@ -112,8 +108,8 @@ where
     }
 }
 
-fn alloc_obj(ty: Arc<TypeInfo>) -> Pin<Box<ObjectInfo>> {
-    let ptr = unsafe { std::alloc::alloc(ty.layout) };
+fn alloc_obj(ty: Type) -> Pin<Box<ObjectInfo>> {
+    let ptr = unsafe { std::alloc::alloc(ty.layout()) };
     Box::pin(ObjectInfo {
         ptr,
         ty,
@@ -126,7 +122,7 @@ impl<O> GcRuntime for MarkSweep<O>
 where
     O: Observer<Event = Event>,
 {
-    fn alloc(&self, ty: &Arc<TypeInfo>) -> GcPtr {
+    fn alloc(&self, ty: &Type) -> GcPtr {
         let object = alloc_obj(ty.clone());
 
         // We want to return a pointer to the `ObjectInfo`, to be used as handle.
@@ -141,7 +137,7 @@ where
         handle
     }
 
-    fn ptr_type(&self, handle: GcPtr) -> Arc<TypeInfo> {
+    fn ptr_type(&self, handle: GcPtr) -> Type {
         let _lock = self.objects.read();
 
         // Convert the handle to our internal representation
@@ -228,11 +224,11 @@ where
                 }
                 true
             } else {
-                unsafe { std::alloc::dealloc(obj.ptr, obj.ty.layout) };
+                unsafe { std::alloc::dealloc(obj.ptr, obj.ty.layout()) };
                 self.observer.event(Event::Deallocation(*h));
                 {
                     let mut stats = self.stats.write();
-                    stats.allocated_memory -= obj.ty.layout.size();
+                    stats.allocated_memory -= obj.ty.layout().size();
                 }
                 false
             }
@@ -285,7 +281,7 @@ where
                 if object_info.ty == *old_ty {
                     let src = unsafe { NonNull::new_unchecked(object_info.ptr) };
                     let dest = unsafe {
-                        NonNull::new_unchecked(std::alloc::alloc_zeroed(conversion.new_ty.layout))
+                        NonNull::new_unchecked(std::alloc::alloc_zeroed(conversion.new_ty.layout()))
                     };
 
                     map_fields(
@@ -297,7 +293,7 @@ where
                         dest,
                     );
 
-                    unsafe { std::alloc::dealloc(src.as_ptr(), old_ty.layout) };
+                    unsafe { std::alloc::dealloc(src.as_ptr(), old_ty.layout()) };
 
                     object_info.set(ObjectInfo {
                         ptr: dest.as_ptr(),
@@ -327,7 +323,7 @@ where
         fn map_fields<O>(
             gc: &MarkSweep<O>,
             new_allocations: &mut Vec<Pin<Box<ObjectInfo>>>,
-            conversions: &HashMap<Arc<TypeInfo>, Conversion>,
+            conversions: &HashMap<Type, Conversion>,
             mapping: &[FieldMapping],
             src: NonNull<u8>,
             dest: NonNull<u8>,
@@ -354,12 +350,12 @@ where
                             src as *mut u8
                         };
 
-                        if old_ty.data.is_struct() {
-                            debug_assert!(new_ty.data.is_struct());
+                        if old_ty.is_struct() {
+                            debug_assert!(new_ty.is_struct());
 
                             // When the name is the same, we are dealing with the same struct,
                             // but different internals
-                            let is_same_struct = old_ty.name == new_ty.name;
+                            let is_same_struct = old_ty.name() == new_ty.name();
 
                             // If the same struct changed, there must also be a conversion
                             let conversion = conversions.get(old_ty);
@@ -404,7 +400,7 @@ where
                                             std::ptr::write_bytes(
                                                 (*object).ptr,
                                                 0,
-                                                new_ty.layout.size(),
+                                                new_ty.layout().size(),
                                             )
                                         };
                                     }
@@ -435,7 +431,7 @@ where
 
                                     // Zero-initialize heap-allocated object
                                     unsafe {
-                                        std::ptr::write_bytes(object.ptr, 0, new_ty.layout.size())
+                                        std::ptr::write_bytes(object.ptr, 0, new_ty.layout().size())
                                     };
 
                                     // Write handle to field
@@ -476,7 +472,7 @@ where
                                             std::ptr::copy_nonoverlapping(
                                                 obj.ptr,
                                                 field_dest,
-                                                obj.ty.layout.size(),
+                                                obj.ty.layout().size(),
                                             )
                                         };
                                     }
@@ -504,7 +500,7 @@ where
                             std::ptr::copy_nonoverlapping(
                                 field_src,
                                 field_dest,
-                                new_ty.layout.size(),
+                                new_ty.layout().size(),
                             )
                         };
                     }
@@ -517,7 +513,7 @@ where
                             let handle = (object.as_ref().deref() as *const _ as RawGcPtr).into();
 
                             // Zero-initialize heap-allocated object
-                            unsafe { std::ptr::write_bytes(object.ptr, 0, new_ty.layout.size()) };
+                            unsafe { std::ptr::write_bytes(object.ptr, 0, new_ty.layout().size()) };
 
                             // Write handle to field
                             let field_dest = field_dest.cast::<GcPtr>();
@@ -557,7 +553,7 @@ struct ObjectInfo {
     pub ptr: *mut u8,
     pub roots: u32,
     pub color: Color,
-    pub ty: Arc<TypeInfo>,
+    pub ty: Type,
 }
 
 /// An `ObjectInfo` is thread-safe.
diff --git a/crates/mun_memory/src/lib.rs b/crates/mun_memory/src/lib.rs
index c91a5ab6d..2e1548759 100644
--- a/crates/mun_memory/src/lib.rs
+++ b/crates/mun_memory/src/lib.rs
@@ -1,23 +1,26 @@
-pub use type_info::{FieldInfo, HasStaticTypeInfo, StructInfo, TypeInfo, TypeInfoData};
+extern crate core;
+
+pub use r#type::{
+    Field, FieldData, HasStaticType, PointerType, StructType, StructTypeBuilder, Type,
+    TypeCollectionStats, TypeKind,
+};
+
+pub mod ffi {
+    pub use super::r#type::ffi::*;
+}
 
 mod cast;
 pub mod diff;
 pub mod gc;
 pub mod mapping;
-mod type_info;
+mod r#type;
 pub mod type_table;
 use thiserror::Error;
 
 pub mod prelude {
     pub use crate::diff::{diff, Diff, FieldDiff, FieldEditKind};
     pub use crate::mapping::{Action, FieldMapping};
-    pub use crate::type_info::{StructInfo, TypeInfo, TypeInfoData};
-}
-
-/// A trait used to obtain a type's fields.
-pub trait TypeFields: Send + Sync {
-    /// Returns the type's fields.
-    fn fields(&self) -> &[FieldInfo];
+    pub use crate::r#type::{Field, PointerType, StructType, Type, TypeKind};
 }
 
 /// An error that can occur when trying to convert from an abi type to an internal type.
diff --git a/crates/mun_memory/src/mapping.rs b/crates/mun_memory/src/mapping.rs
index 0ba870fb9..52ed57a6a 100644
--- a/crates/mun_memory/src/mapping.rs
+++ b/crates/mun_memory/src/mapping.rs
@@ -1,29 +1,25 @@
 use crate::{
     diff::{diff, Diff, FieldDiff, FieldEditKind},
     gc::GcPtr,
-    type_info::TypeInfo,
-    TypeFields,
-};
-use std::{
-    collections::{HashMap, HashSet},
-    sync::Arc,
+    r#type::Type,
 };
+use std::collections::{HashMap, HashSet};
 
 pub struct Mapping {
-    pub deletions: HashSet<Arc<TypeInfo>>,
-    pub conversions: HashMap<Arc<TypeInfo>, Conversion>,
-    pub identical: Vec<(Arc<TypeInfo>, Arc<TypeInfo>)>,
+    pub deletions: HashSet<Type>,
+    pub conversions: HashMap<Type, Conversion>,
+    pub identical: Vec<(Type, Type)>,
 }
 
 pub struct Conversion {
     pub field_mapping: Vec<FieldMapping>,
-    pub new_ty: Arc<TypeInfo>,
+    pub new_ty: Type,
 }
 
 /// Description of the mapping of a single field. When stored together with the new index, this
 /// provides all information necessary for a mapping function.
 pub struct FieldMapping {
-    pub new_ty: Arc<TypeInfo>,
+    pub new_ty: Type,
     pub new_offset: usize,
     pub action: Action,
 }
@@ -31,19 +27,14 @@ pub struct FieldMapping {
 /// The `Action` to take when mapping memory from A to B.
 #[derive(Eq, PartialEq)]
 pub enum Action {
-    Cast {
-        old_offset: usize,
-        old_ty: Arc<TypeInfo>,
-    },
-    Copy {
-        old_offset: usize,
-    },
+    Cast { old_offset: usize, old_ty: Type },
+    Copy { old_offset: usize },
     Insert,
 }
 
 impl Mapping {
     #[allow(clippy::mutable_key_type)]
-    pub fn new(old: &[Arc<TypeInfo>], new: &[Arc<TypeInfo>]) -> Self {
+    pub fn new(old: &[Type], new: &[Type]) -> Self {
         let diff = diff(old, new);
 
         let mut conversions = HashMap::new();
@@ -88,7 +79,7 @@ impl Mapping {
         let mut new_candidates: HashSet<_> = new
             .iter()
             // Filter non-struct types
-            .filter(|ty| ty.data.is_struct())
+            .filter(|ty| ty.is_struct())
             // Filter inserted structs
             .filter(|ty| !insertions.contains(*ty))
             .cloned()
@@ -97,7 +88,7 @@ impl Mapping {
         let mut old_candidates: HashSet<_> = old
             .iter()
             // Filter non-struct types
-            .filter(|ty| ty.data.is_struct())
+            .filter(|ty| ty.is_struct())
             // Filter deleted structs
             .filter(|ty| !deletions.contains(*ty))
             // Filter edited types
@@ -144,12 +135,11 @@ impl Mapping {
 /// # Safety
 ///
 /// Expects the `diff` to be based on `old_ty` and `new_ty`. If not, it causes undefined behavior.
-pub unsafe fn field_mapping(
-    old_ty: &Arc<TypeInfo>,
-    new_ty: &Arc<TypeInfo>,
-    diff: &[FieldDiff],
-) -> Conversion {
-    let old_fields = old_ty.fields();
+pub unsafe fn field_mapping(old_ty: &Type, new_ty: &Type, diff: &[FieldDiff]) -> Conversion {
+    let old_fields = old_ty
+        .as_struct()
+        .map(|s| s.fields().iter().collect())
+        .unwrap_or_else(Vec::new);
 
     let deletions: HashSet<usize> = diff
         .iter()
@@ -237,7 +227,10 @@ pub unsafe fn field_mapping(
         }
     }
 
-    let new_fields = new_ty.fields();
+    let new_fields = new_ty
+        .as_struct()
+        .map(|s| Vec::from_iter(s.fields().iter()))
+        .unwrap_or_else(Vec::new);
     Conversion {
         field_mapping: mapping
             .into_iter()
@@ -245,18 +238,15 @@ pub unsafe fn field_mapping(
             .map(|(new_index, desc)| {
                 let old_offset = desc
                     .old_index
-                    .map(|idx| usize::from(old_fields.get_unchecked(idx).offset));
+                    .map(|idx| old_fields.get_unchecked(idx).offset());
 
                 FieldMapping {
-                    new_ty: new_fields.get_unchecked(new_index).type_info.clone(),
-                    new_offset: usize::from(new_fields.get_unchecked(new_index).offset),
+                    new_ty: new_fields.get_unchecked(new_index).ty(),
+                    new_offset: new_fields.get_unchecked(new_index).offset(),
                     action: match desc.action {
                         ActionDesc::Cast => Action::Cast {
                             old_offset: old_offset.unwrap(),
-                            old_ty: old_fields
-                                .get_unchecked(desc.old_index.unwrap())
-                                .type_info
-                                .clone(),
+                            old_ty: old_fields.get_unchecked(desc.old_index.unwrap()).ty(),
                         },
                         ActionDesc::Copy => Action::Copy {
                             old_offset: old_offset.unwrap(),
diff --git a/crates/mun_memory/src/type/ffi/mod.rs b/crates/mun_memory/src/type/ffi/mod.rs
new file mode 100644
index 000000000..9fc9b4db7
--- /dev/null
+++ b/crates/mun_memory/src/type/ffi/mod.rs
@@ -0,0 +1,529 @@
+#![allow(dead_code)]
+
+//! Defines an FFI compatible type interface for type information.
+
+use std::{
+    ffi::c_void, ffi::CString, mem::ManuallyDrop, ops::Deref, os::raw::c_char, ptr, ptr::NonNull,
+    sync::atomic::Ordering, sync::Arc,
+};
+
+use abi::Guid;
+use capi_utils::{mun_error_try, try_deref_mut, ErrorHandle};
+pub use r#pointer::PointerInfo;
+pub use r#struct::{Field, Fields, StructInfo};
+
+use crate::r#type::{PointerData, StructData, TypeData, TypeDataKind, TypeDataStore};
+
+mod pointer;
+mod primitive;
+mod r#struct;
+
+/// A [`Type`] holds information about a mun type.
+#[repr(C)]
+#[derive(Copy, Clone)]
+pub struct Type(*const c_void, *const c_void);
+
+impl From<crate::Type> for Type {
+    fn from(ty: crate::Type) -> Self {
+        let ty = ManuallyDrop::new(ty);
+        Type(
+            ty.inner.as_ptr() as *const _,
+            Arc::as_ptr(&ty.store) as *const _,
+        )
+    }
+}
+
+impl Type {
+    /// Returns the store associated with the Type or
+    unsafe fn store(&self) -> Result<ManuallyDrop<Arc<TypeDataStore>>, String> {
+        if self.1.is_null() {
+            return Err(String::from("null pointer"));
+        }
+
+        Ok(ManuallyDrop::new(Arc::from_raw(
+            self.1 as *const TypeDataStore,
+        )))
+    }
+
+    /// Returns the store associated with the Type or
+    unsafe fn inner(&self) -> Result<&TypeData, String> {
+        (self.0 as *const TypeData)
+            .as_ref()
+            .ok_or_else(|| String::from("null pointer"))
+    }
+
+    /// Converts this FFI type into an owned Rust type. This transfers the ownership from the FFI
+    /// type back to Rust.
+    ///
+    /// # Safety
+    ///
+    /// The caller must ensure that self contains valid pointers.
+    pub unsafe fn to_owned(self) -> Result<super::Type, String> {
+        if self.0.is_null() {
+            return Err(String::from("null pointer"));
+        }
+
+        if self.1.is_null() {
+            return Err(String::from("null pointer"));
+        }
+
+        Ok(super::Type {
+            inner: NonNull::new_unchecked(self.0 as *mut _),
+            store: Arc::from_raw(self.1 as *const _),
+        })
+    }
+
+    /// Returns an invalid Type
+    pub const fn null() -> Self {
+        Self(ptr::null(), ptr::null())
+    }
+}
+
+/// Notifies the runtime that the specified type is no longer used. Any use of the type after
+/// calling this function results in undefined behavior.
+///
+/// # Safety
+///
+/// This function results in undefined behavior if the passed in `Type` has been deallocated in a
+/// previous call to [`mun_type_release`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_type_release(ty: Type) -> ErrorHandle {
+    // Transfer ownership to Rust and immediately drop the instance
+    let _ = mun_error_try!(ty
+        .to_owned()
+        .map_err(|e| format!("invalid argument 'ty': {e}")));
+
+    ErrorHandle::default()
+}
+
+/// Increments the usage count of the specified type.
+///
+/// # Safety
+///
+/// This function results in undefined behavior if the passed in `Type` has been deallocated in a
+/// previous call to [`mun_type_release`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_type_add_reference(ty: Type) -> ErrorHandle {
+    let store = mun_error_try!(ty
+        .store()
+        .map_err(|e| format!("invalid argument 'ty': {e}")));
+    let inner = mun_error_try!(ty
+        .inner()
+        .map_err(|e| format!("invalid argument 'ty': {e}")));
+
+    // Release the references owned by the type
+    inner.external_references.fetch_add(1, Ordering::Relaxed);
+    Arc::increment_strong_count(Arc::as_ptr(&store));
+
+    ErrorHandle::default()
+}
+
+/// Retrieves the type's name.
+///
+/// # Safety
+///
+/// The caller is responsible for calling `mun_string_destroy` on the return pointer - if it is not
+/// null.
+///
+/// This function results in undefined behavior if the passed in `Type` has been deallocated in a
+/// previous call to [`mun_type_release`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_type_name(ty: Type, name: *mut *const c_char) -> ErrorHandle {
+    let inner = mun_error_try!(ty
+        .inner()
+        .map_err(|e| format!("invalid argument 'ty': {e}")));
+    let name = try_deref_mut!(name);
+    *name = CString::new(inner.name.clone()).unwrap().into_raw();
+    ErrorHandle::default()
+}
+
+/// Compares two different Types. Returns `true` if the two types are equal. If either of the two
+/// types is invalid because for instance it contains null pointers this function returns `false`.
+///
+/// # Safety
+///
+/// This function results in undefined behavior if the passed in `Type`s have been deallocated in a
+/// previous call to [`mun_type_release`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_type_equal(a: Type, b: Type) -> bool {
+    match (a.inner(), b.inner()) {
+        (Ok(a), Ok(b)) => a == b,
+        _ => false,
+    }
+}
+
+/// Returns the storage size required for a type. The storage size does not include any padding to
+/// align the size. Call [`mun_type_alignment`] to request the alignment of the type.
+///
+/// # Safety
+///
+/// This function results in undefined behavior if the passed in `Type`s have been deallocated in a
+/// previous call to [`mun_type_release`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_type_size(ty: Type, size: *mut usize) -> ErrorHandle {
+    let size = try_deref_mut!(size);
+    let inner = mun_error_try!(ty
+        .inner()
+        .map_err(|e| format!("invalid argument 'ty': {e}")));
+    *size = inner.layout.size();
+    ErrorHandle::default()
+}
+
+/// Returns the alignment requirements of the type.
+///
+/// # Safety
+///
+/// This function results in undefined behavior if the passed in `Type`s have been deallocated in a
+/// previous call to [`mun_type_release`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_type_alignment(ty: Type, align: *mut usize) -> ErrorHandle {
+    let align = try_deref_mut!(align);
+    let inner = mun_error_try!(ty
+        .inner()
+        .map_err(|e| format!("invalid argument 'ty': {e}")));
+    *align = inner.layout.align();
+    ErrorHandle::default()
+}
+
+/// Returns a new [`Type`] that is a pointer to the specified type.
+///
+/// # Safety
+///
+/// This function results in undefined behavior if the passed in `Type`s have been deallocated in a
+/// previous call to [`mun_type_release`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_type_pointer_type(
+    ty: Type,
+    mutable: bool,
+    pointer_ty: *mut Type,
+) -> ErrorHandle {
+    let pointer_ty = try_deref_mut!(pointer_ty);
+    let store = mun_error_try!(ty
+        .store()
+        .map_err(|e| format!("invalid argument 'ty': {e}")));
+    let inner = mun_error_try!(ty
+        .inner()
+        .map_err(|e| format!("invalid argument 'ty': {e}")));
+    *pointer_ty = inner.pointer_type(mutable, &store).into();
+    ErrorHandle::default()
+}
+
+/// An enum that defines the kind of type.
+#[repr(u8)]
+pub enum TypeKind {
+    Primitive(Guid),
+    Pointer(r#pointer::PointerInfo),
+    Struct(r#struct::StructInfo),
+}
+
+/// Returns information about what kind of type this is.
+///
+/// # Safety
+///
+/// This function results in undefined behavior if the passed in `Type`s have been deallocated in a
+/// previous call to [`mun_type_release`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_type_kind(ty: Type, kind: *mut TypeKind) -> ErrorHandle {
+    let kind = try_deref_mut!(kind);
+    let store = mun_error_try!(ty
+        .store()
+        .map_err(|e| format!("invalid argument 'ty': {e}")));
+    let inner = mun_error_try!(ty
+        .inner()
+        .map_err(|e| format!("invalid argument 'ty': {e}")));
+
+    *kind = match &inner.data {
+        TypeDataKind::Primitive(guid) => TypeKind::Primitive(*guid),
+        TypeDataKind::Pointer(pointer) => TypeKind::Pointer(PointerInfo(
+            (pointer as *const PointerData).cast(),
+            Arc::as_ptr(ManuallyDrop::deref(&store)) as *const _,
+        )),
+        TypeDataKind::Struct(s) => TypeKind::Struct(StructInfo(
+            (s as *const StructData).cast(),
+            Arc::as_ptr(ManuallyDrop::deref(&store)) as *const _,
+        )),
+        TypeDataKind::Uninitialized => unreachable!(),
+    };
+
+    ErrorHandle::default()
+}
+
+/// An array of [`Type`]s.
+///
+/// The `Types` struct owns the `Type`s it references. Ownership of the `Type` can be shared by
+/// calling [`mun_type_add_reference`].
+///
+/// This is backed by a dynamically allocated array. Ownership is transferred via this struct
+/// and its contents must be destroyed with [`mun_types_destroy`].
+#[repr(C)]
+pub struct Types {
+    pub types: *const Type,
+    pub count: usize,
+}
+
+impl From<Vec<Type>> for Types {
+    fn from(mut vec: Vec<Type>) -> Self {
+        vec.shrink_to_fit();
+        let vec = ManuallyDrop::new(vec);
+        Types {
+            types: if vec.is_empty() {
+                ptr::null()
+            } else {
+                vec.as_ptr()
+            },
+            count: vec.len(),
+        }
+    }
+}
+
+/// Destroys the contents of a [`Types`] struct.
+///
+/// # Safety
+///
+/// This function results in undefined behavior if the passed in `Types` has been deallocated
+/// by a previous call to [`mun_types_destroy`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_types_destroy(types: Types) -> ErrorHandle {
+    if types.types.is_null() && types.count > 0 {
+        return ErrorHandle::new("invalid argument 'types': null pointer");
+    } else if types.count > 0 {
+        let types = Vec::from_raw_parts(types.types as *mut Type, types.count, types.count);
+        for ty in types.into_iter() {
+            // Take ownership of the stored type and drop it
+            drop(mun_error_try!(ty
+                .to_owned()
+                .map_err(|e| format!("fields contain invalid type: {e}"))));
+        }
+    }
+    ErrorHandle::default()
+}
+
+#[cfg(test)]
+mod test {
+    use std::ffi::{c_void, CStr, CString};
+    use std::mem::MaybeUninit;
+    use std::ptr;
+
+    use crate::ffi::{mun_types_destroy, Types};
+    use capi_utils::{
+        assert_error, assert_error_snapshot, assert_getter1, assert_getter2, mun_string_destroy,
+    };
+
+    use crate::r#type::ffi::{
+        mun_type_add_reference, mun_type_alignment, mun_type_pointer_type, mun_type_size,
+    };
+    use crate::HasStaticType;
+
+    use super::{
+        mun_type_equal, mun_type_name, mun_type_release,
+        primitive::{mun_type_primitive, PrimitiveType},
+        Type,
+    };
+
+    const FFI_TYPE_NULL_INNER: Type = Type(ptr::null(), 0xDEAD as *const c_void);
+    const FFI_TYPE_NULL_STORE: Type = Type(0xDEAD as *const c_void, ptr::null());
+    const FFI_TYPE_NULL: Type = Type(ptr::null(), ptr::null());
+
+    #[test]
+    fn test_mun_type_release_null_type() {
+        assert_error!(unsafe { mun_type_release(FFI_TYPE_NULL_INNER) });
+        assert_error!(unsafe { mun_type_release(FFI_TYPE_NULL_STORE) });
+    }
+
+    #[test]
+    fn test_mun_type_add_reference_null_type() {
+        assert_error!(unsafe { mun_type_add_reference(FFI_TYPE_NULL_INNER) });
+        assert_error!(unsafe { mun_type_add_reference(FFI_TYPE_NULL_STORE) });
+    }
+
+    #[test]
+    fn test_mun_type_equal() {
+        let ffi_f32 = mun_type_primitive(PrimitiveType::F32);
+        let ffi_i32 = mun_type_primitive(PrimitiveType::I32);
+
+        assert!(unsafe { mun_type_equal(ffi_f32, ffi_f32) });
+        assert!(unsafe { mun_type_equal(ffi_i32, ffi_i32) });
+        assert!(!unsafe { mun_type_equal(ffi_f32, ffi_i32) });
+        assert!(!unsafe { mun_type_equal(ffi_i32, ffi_f32) });
+        assert!(!unsafe { mun_type_equal(ffi_f32, FFI_TYPE_NULL) });
+        assert!(!unsafe { mun_type_equal(FFI_TYPE_NULL, ffi_f32) });
+        assert!(!unsafe { mun_type_equal(FFI_TYPE_NULL, FFI_TYPE_NULL) });
+
+        unsafe { mun_type_release(ffi_f32) };
+        unsafe { mun_type_release(ffi_i32) };
+    }
+
+    #[test]
+    fn test_mun_type_name() {
+        let ffi_f32 = mun_type_primitive(PrimitiveType::F32);
+        let ffi_empty = mun_type_primitive(PrimitiveType::Empty);
+
+        assert_getter1!(mun_type_name(ffi_f32, f32_name));
+        assert_getter1!(mun_type_name(ffi_empty, empty_name));
+
+        assert_eq!(
+            unsafe { CStr::from_ptr(f32_name) },
+            CString::new("core::f32").unwrap().as_ref()
+        );
+        assert_eq!(
+            unsafe { CStr::from_ptr(empty_name) },
+            CString::new("core::empty").unwrap().as_ref()
+        );
+
+        unsafe { mun_string_destroy(empty_name) };
+        unsafe { mun_string_destroy(f32_name) };
+        unsafe { mun_type_release(ffi_empty) };
+        unsafe { mun_type_release(ffi_f32) };
+    }
+
+    #[test]
+    fn test_mun_type_name_invalid_type() {
+        let mut name = MaybeUninit::uninit();
+        assert_error_snapshot!(
+            unsafe { mun_type_name(FFI_TYPE_NULL, name.as_mut_ptr()) },
+            @r###""invalid argument \'ty\': null pointer""###
+        );
+
+        let ffi_i8 = mun_type_primitive(PrimitiveType::I8);
+        assert_error_snapshot!(
+            unsafe { mun_type_name(ffi_i8, ptr::null_mut()) },
+            @r###""invalid argument \'name\': null pointer""###
+        );
+
+        unsafe { mun_type_release(ffi_i8) };
+    }
+
+    #[test]
+    fn test_mun_type_size() {
+        let ffi_i8 = mun_type_primitive(PrimitiveType::I8);
+        let ffi_u16 = mun_type_primitive(PrimitiveType::U16);
+        let ffi_i32 = mun_type_primitive(PrimitiveType::I32);
+        let ffi_u64 = mun_type_primitive(PrimitiveType::U64);
+
+        assert_size(ffi_i8, 1);
+        assert_size(ffi_u16, 2);
+        assert_size(ffi_i32, 4);
+        assert_size(ffi_u64, 8);
+
+        unsafe { mun_type_release(ffi_u64) };
+        unsafe { mun_type_release(ffi_i32) };
+        unsafe { mun_type_release(ffi_u16) };
+        unsafe { mun_type_release(ffi_i8) };
+
+        fn assert_size(ty: Type, expected_size: usize) {
+            let mut size = MaybeUninit::uninit();
+            assert!(unsafe { mun_type_size(ty, size.as_mut_ptr()) }.is_ok());
+            assert_eq!(unsafe { size.assume_init() }, expected_size);
+        }
+    }
+
+    #[test]
+    fn test_mun_type_size_invalid_null() {
+        let mut size = MaybeUninit::uninit();
+        assert_error!(unsafe { mun_type_size(FFI_TYPE_NULL, size.as_mut_ptr()) });
+
+        let ffi_i8 = mun_type_primitive(PrimitiveType::I8);
+        assert_error!(unsafe { mun_type_size(ffi_i8, ptr::null_mut()) });
+        unsafe { mun_type_release(ffi_i8) };
+    }
+
+    #[test]
+    fn test_mun_type_alignment() {
+        let ffi_i8 = mun_type_primitive(PrimitiveType::I8);
+        let ffi_u16 = mun_type_primitive(PrimitiveType::U16);
+        let ffi_i32 = mun_type_primitive(PrimitiveType::I32);
+        let ffi_u64 = mun_type_primitive(PrimitiveType::U64);
+
+        assert_alignment(ffi_i8, 1);
+        assert_alignment(ffi_u16, 2);
+        assert_alignment(ffi_i32, 4);
+        assert_alignment(ffi_u64, 8);
+
+        unsafe { mun_type_release(ffi_u64) };
+        unsafe { mun_type_release(ffi_i32) };
+        unsafe { mun_type_release(ffi_u16) };
+        unsafe { mun_type_release(ffi_i8) };
+
+        fn assert_alignment(ty: Type, expected_alignment: usize) {
+            let mut align = MaybeUninit::uninit();
+            assert!(unsafe { mun_type_alignment(ty, align.as_mut_ptr()) }.is_ok());
+            assert_eq!(unsafe { align.assume_init() }, expected_alignment);
+        }
+    }
+
+    #[test]
+    fn test_mun_type_alignment_invalid_null() {
+        let mut size = MaybeUninit::uninit();
+        assert_error_snapshot!(
+            unsafe { mun_type_alignment(FFI_TYPE_NULL, size.as_mut_ptr()) },
+            @r###""invalid argument \'ty\': null pointer""###
+        );
+
+        let ffi_i8 = mun_type_primitive(PrimitiveType::I8);
+        assert_error_snapshot!(
+            unsafe { mun_type_alignment(ffi_i8, ptr::null_mut()) },
+            @r###""invalid argument \'align\': null pointer""###
+        );
+
+        unsafe { mun_type_release(ffi_i8) };
+    }
+
+    #[test]
+    fn test_mun_type_pointer_type() {
+        let ffi_u64 = mun_type_primitive(PrimitiveType::U64);
+
+        assert_getter2!(mun_type_pointer_type(ffi_u64, true, ffi_u64_pointer));
+        let rust_u64_pointer = unsafe { ffi_u64_pointer.to_owned() }.unwrap();
+        let pointer_info = rust_u64_pointer
+            .as_pointer()
+            .expect("type is not a pointer");
+        assert_eq!(&pointer_info.pointee(), u64::type_info());
+        assert!(&pointer_info.is_mutable());
+
+        unsafe { mun_type_release(ffi_u64) };
+    }
+
+    #[test]
+    fn test_mun_type_pointer_type_invalid_null() {
+        let mut ffi_u64_pointer = MaybeUninit::uninit();
+        assert_error_snapshot!(
+            unsafe { mun_type_pointer_type(FFI_TYPE_NULL, true, ffi_u64_pointer.as_mut_ptr()) },
+            @r###""invalid argument \'ty\': null pointer""###
+        );
+
+        let ffi_u64 = mun_type_primitive(PrimitiveType::U64);
+        assert_error_snapshot!(
+            unsafe { mun_type_pointer_type(ffi_u64, true, ptr::null_mut()) },
+            @r###""invalid argument \'pointer_ty\': null pointer""###
+        );
+        unsafe { mun_type_release(ffi_u64) };
+    }
+
+    #[test]
+    fn test_mun_types_destroy() {
+        let types: Types = [i32::type_info().clone(), f32::type_info().clone()]
+            .iter()
+            .map(|ty| ty.clone().into())
+            .collect::<Vec<_>>()
+            .into();
+        assert!(unsafe { mun_types_destroy(types) }.is_ok());
+    }
+
+    #[test]
+    fn test_mun_types_destroy_invalid_ptr() {
+        let types = Types {
+            types: ptr::null(),
+            count: 1,
+        };
+        assert_error!(unsafe { mun_types_destroy(types) });
+    }
+
+    #[test]
+    fn test_mun_types_destroy_empty() {
+        let types = Types {
+            types: ptr::null(),
+            count: 0,
+        };
+        assert!(unsafe { mun_types_destroy(types) }.is_ok());
+    }
+}
diff --git a/crates/mun_memory/src/type/ffi/pointer.rs b/crates/mun_memory/src/type/ffi/pointer.rs
new file mode 100644
index 000000000..4a40932ed
--- /dev/null
+++ b/crates/mun_memory/src/type/ffi/pointer.rs
@@ -0,0 +1,202 @@
+use std::{ffi::c_void, mem::ManuallyDrop, ops::Deref, sync::Arc};
+
+use capi_utils::{mun_error_try, try_deref_mut, ErrorHandle};
+
+use crate::r#type::{PointerData, Type as RustType, TypeDataStore};
+
+use super::Type;
+
+/// Additional information of a pointer [`Type`].
+///
+/// Ownership of this type lies with the [`Type`] that created this instance. As long as the
+/// original type is not released through [`mun_type_release`] this type stays alive.
+#[repr(C)]
+#[derive(Copy, Clone)]
+pub struct PointerInfo(pub(super) *const c_void, pub(super) *const c_void);
+
+impl<'t> From<super::super::PointerType<'t>> for PointerInfo {
+    fn from(ty: super::super::PointerType<'t>) -> Self {
+        PointerInfo(
+            (ty.inner as *const PointerData).cast(),
+            (&ty.store as *const &Arc<TypeDataStore>).cast(),
+        )
+    }
+}
+
+impl PointerInfo {
+    /// Returns the store associated with this instance
+    unsafe fn store(&self) -> Result<ManuallyDrop<Arc<TypeDataStore>>, String> {
+        if self.1.is_null() {
+            return Err(String::from("null pointer"));
+        }
+
+        Ok(ManuallyDrop::new(Arc::from_raw(
+            self.1 as *const TypeDataStore,
+        )))
+    }
+
+    /// Returns the pointer ino associated with the Type
+    unsafe fn inner(&self) -> Result<&PointerData, String> {
+        match (self.0 as *const PointerData).as_ref() {
+            Some(store) => Ok(store),
+            None => Err(String::from("null pointer")),
+        }
+    }
+
+    /// Converts from C FFI type to a Rust type.
+    unsafe fn to_rust<'a>(self) -> Result<super::super::PointerType<'a>, String> {
+        match (
+            (self.0 as *const PointerData).as_ref(),
+            (self.1 as *const Arc<TypeDataStore>).as_ref(),
+        ) {
+            (Some(inner), Some(store)) => Ok(super::super::PointerType { inner, store }),
+            _ => Err(String::from("null pointer")),
+        }
+    }
+}
+
+/// Returns the type that this instance points to. Ownership is transferred if this function returns
+/// successfully.
+///
+/// # Safety
+///
+/// This function results in undefined behavior if the passed in `PointerType` has been deallocated
+/// by a previous call to [`mun_type_release`].
+pub unsafe extern "C" fn mun_pointer_type_pointee(
+    ty: PointerInfo,
+    pointee: *mut Type,
+) -> ErrorHandle {
+    let store = mun_error_try!(ty
+        .store()
+        .map_err(|e| format!("invalid argument 'ty': {e}")));
+    let ty = mun_error_try!(ty
+        .inner()
+        .map_err(|e| format!("invalid argument 'ty': {e}")));
+    let pointee = try_deref_mut!(pointee);
+    *pointee = RustType::new_unchecked(ty.pointee, ManuallyDrop::deref(&store).clone()).into();
+    ErrorHandle::default()
+}
+
+/// Returns true if this is a mutable pointer.
+///
+/// # Safety
+///
+/// This function results in undefined behavior if the passed in `PointerType` has been deallocated
+/// by a previous call to [`mun_type_release`].
+pub unsafe extern "C" fn mun_pointer_is_mutable(
+    ty: PointerInfo,
+    mutable: *mut bool,
+) -> ErrorHandle {
+    let ty = mun_error_try!(ty
+        .inner()
+        .map_err(|e| format!("invalid argument 'ty': {e}")));
+    let mutable = try_deref_mut!(mutable);
+    *mutable = ty.mutable;
+    ErrorHandle::default()
+}
+
+#[cfg(test)]
+mod test {
+    use std::mem::MaybeUninit;
+    use std::ptr;
+
+    use capi_utils::{assert_error_snapshot, assert_getter1, assert_getter2};
+
+    use super::super::{
+        mun_type_equal, mun_type_kind, mun_type_pointer_type, mun_type_release,
+        pointer::{mun_pointer_is_mutable, mun_pointer_type_pointee, PointerInfo},
+        primitive::{mun_type_primitive, PrimitiveType},
+        Type, TypeKind,
+    };
+
+    /// Returns the pointer type of the specified type. Asserts if that fails.
+    unsafe fn pointer_type(ty: Type, mutable: bool) -> (Type, PointerInfo) {
+        assert_getter2!(mun_type_pointer_type(ty, mutable, ptr_ty));
+
+        assert_getter1!(mun_type_kind(ptr_ty, ty_kind));
+        let pointer_ty = match ty_kind {
+            TypeKind::Pointer(p) => p,
+            _ => panic!("invalid type kind for pointer"),
+        };
+
+        (ty, pointer_ty)
+    }
+
+    #[test]
+    fn test_mun_pointer_type_pointee() {
+        let ffi_f32 = mun_type_primitive(PrimitiveType::F32);
+        let (ffi_f32_ptr, ptr_info) = unsafe { pointer_type(ffi_f32, true) };
+
+        assert_getter1!(mun_pointer_type_pointee(ptr_info, pointee_ty));
+        assert!(unsafe { mun_type_equal(pointee_ty, ffi_f32) });
+
+        unsafe { mun_type_release(pointee_ty) };
+        unsafe { mun_type_release(ffi_f32_ptr) };
+        unsafe { mun_type_release(ffi_f32) };
+    }
+
+    #[test]
+    fn test_mun_pointer_type_pointee_invalid_null() {
+        let mut pointee_ty = MaybeUninit::uninit();
+        assert_error_snapshot!(
+            unsafe {
+                mun_pointer_type_pointee(
+                    PointerInfo(ptr::null(), ptr::null()),
+                    pointee_ty.as_mut_ptr(),
+                )
+            },
+            @r###""invalid argument \'ty\': null pointer""###
+        );
+
+        let ffi_f32 = mun_type_primitive(PrimitiveType::F32);
+        let (ffi_f32_ptr, ptr_info) = unsafe { pointer_type(ffi_f32, true) };
+        assert_error_snapshot!(
+            unsafe { mun_pointer_type_pointee(ptr_info, ptr::null_mut()) },
+            @r###""invalid argument \'pointee\': null pointer""###
+        );
+
+        unsafe { mun_type_release(ffi_f32_ptr) };
+        unsafe { mun_type_release(ffi_f32) };
+    }
+
+    #[test]
+    fn test_mun_pointer_type_is_mutable() {
+        let ffi_f32 = mun_type_primitive(PrimitiveType::F32);
+        let (ffi_f32_immutable_ptr, immutable_ptr_info) = unsafe { pointer_type(ffi_f32, false) };
+        let (ffi_f32_mutable_ptr, mutable_ptr_info) = unsafe { pointer_type(ffi_f32, true) };
+
+        assert_getter1!(mun_pointer_is_mutable(immutable_ptr_info, is_mutable));
+        assert!(!is_mutable);
+
+        assert_getter1!(mun_pointer_is_mutable(mutable_ptr_info, is_mutable));
+        assert!(is_mutable);
+
+        unsafe { mun_type_release(ffi_f32_mutable_ptr) };
+        unsafe { mun_type_release(ffi_f32_immutable_ptr) };
+        unsafe { mun_type_release(ffi_f32) };
+    }
+
+    #[test]
+    fn test_mun_pointer_type_is_mutable_invalid_null() {
+        let mut is_mutable = MaybeUninit::uninit();
+        assert_error_snapshot!(
+            unsafe {
+                mun_pointer_is_mutable(
+                    PointerInfo(ptr::null(), ptr::null()),
+                    is_mutable.as_mut_ptr(),
+                )
+            },
+            @r###""invalid argument \'ty\': null pointer""###
+        );
+
+        let ffi_f32 = mun_type_primitive(PrimitiveType::F32);
+        let (ffi_f32_ptr, ptr_info) = unsafe { pointer_type(ffi_f32, true) };
+        assert_error_snapshot!(
+            unsafe { mun_pointer_is_mutable(ptr_info, ptr::null_mut()) },
+            @r###""invalid argument \'mutable\': null pointer""###
+        );
+
+        unsafe { mun_type_release(ffi_f32_ptr) };
+        unsafe { mun_type_release(ffi_f32) };
+    }
+}
diff --git a/crates/mun_memory/src/type/ffi/primitive.rs b/crates/mun_memory/src/type/ffi/primitive.rs
new file mode 100644
index 000000000..8e807a250
--- /dev/null
+++ b/crates/mun_memory/src/type/ffi/primitive.rs
@@ -0,0 +1,92 @@
+use crate::{r#type::ffi::Type, HasStaticType};
+
+/// Types of primitives supported by Mun.
+#[repr(u8)]
+#[derive(Clone, Copy)]
+#[allow(missing_docs)]
+pub enum PrimitiveType {
+    Bool,
+    U8,
+    U16,
+    U32,
+    U64,
+    U128,
+    I8,
+    I16,
+    I32,
+    I64,
+    I128,
+    F32,
+    F64,
+    Empty,
+    Void,
+}
+
+/// Returns a [`Type`] that represents the specified primitive type.
+#[no_mangle]
+pub extern "C" fn mun_type_primitive(primitive_type: PrimitiveType) -> Type {
+    match primitive_type {
+        PrimitiveType::Bool => bool::type_info(),
+        PrimitiveType::U8 => u8::type_info(),
+        PrimitiveType::U16 => u16::type_info(),
+        PrimitiveType::U32 => u32::type_info(),
+        PrimitiveType::U64 => u64::type_info(),
+        PrimitiveType::U128 => u128::type_info(),
+        PrimitiveType::I8 => i8::type_info(),
+        PrimitiveType::I16 => i16::type_info(),
+        PrimitiveType::I32 => i32::type_info(),
+        PrimitiveType::I64 => i64::type_info(),
+        PrimitiveType::I128 => i128::type_info(),
+        PrimitiveType::F32 => f32::type_info(),
+        PrimitiveType::F64 => f64::type_info(),
+        PrimitiveType::Empty => <()>::type_info(),
+        PrimitiveType::Void => <std::ffi::c_void>::type_info(),
+    }
+    .clone()
+    .into()
+}
+
+#[cfg(test)]
+mod test {
+    use super::{
+        super::{mun_type_kind, TypeKind},
+        mun_type_primitive,
+        PrimitiveType::{self, *},
+    };
+    use crate::HasStaticType;
+    use capi_utils::assert_getter1;
+
+    #[test]
+    fn test_primitives() {
+        test_primitive::<bool>(Bool);
+        test_primitive::<u8>(U8);
+        test_primitive::<u16>(U16);
+        test_primitive::<u32>(U32);
+        test_primitive::<u64>(U64);
+        test_primitive::<u128>(U128);
+        test_primitive::<i8>(I8);
+        test_primitive::<i16>(I16);
+        test_primitive::<i32>(I32);
+        test_primitive::<i64>(I64);
+        test_primitive::<i128>(I128);
+        test_primitive::<f32>(F32);
+        test_primitive::<f64>(F64);
+        test_primitive::<()>(Empty);
+        test_primitive::<std::ffi::c_void>(Void);
+
+        fn test_primitive<T: HasStaticType>(primitive_type: PrimitiveType) {
+            let ffi_ty = mun_type_primitive(primitive_type);
+
+            assert_getter1!(mun_type_kind(ffi_ty, ffi_kind));
+            let guid = match ffi_kind {
+                TypeKind::Primitive(guid) => guid,
+                _ => panic!("invalid type kind for primitive"),
+            };
+
+            let rust_ty = unsafe { ffi_ty.to_owned() }.unwrap();
+            let static_ty = T::type_info();
+            assert_eq!(&rust_ty, static_ty);
+            assert_eq!(static_ty.as_concrete().unwrap(), &guid);
+        }
+    }
+}
diff --git a/crates/mun_memory/src/type/ffi/struct.rs b/crates/mun_memory/src/type/ffi/struct.rs
new file mode 100644
index 000000000..fce0c5e01
--- /dev/null
+++ b/crates/mun_memory/src/type/ffi/struct.rs
@@ -0,0 +1,533 @@
+use std::{
+    ffi::{c_void, CString},
+    mem::ManuallyDrop,
+    ops::Deref,
+    os::raw::c_char,
+    ptr, slice,
+    sync::Arc,
+};
+
+use abi::Guid;
+use capi_utils::{mun_error_try, try_deref_mut, ErrorHandle};
+
+use crate::{
+    r#type::ffi::Type,
+    r#type::{StructData, StructType as RustStructType, Type as RustType, TypeDataStore},
+    FieldData,
+};
+
+/// Additional information of a struct [`Type`].
+///
+/// Ownership of this type lies with the [`Type`] that created this instance. As long as the
+/// original type is not released through [`mun_type_release`] this type stays alive.
+#[repr(C)]
+#[derive(Copy, Clone)]
+pub struct StructInfo(pub(super) *const c_void, pub(super) *const c_void);
+
+impl<'t> From<RustStructType<'t>> for StructInfo {
+    fn from(ty: RustStructType<'t>) -> Self {
+        StructInfo(
+            (ty.inner as *const StructData).cast(),
+            (&ty.store as *const &Arc<TypeDataStore>).cast(),
+        )
+    }
+}
+
+impl StructInfo {
+    /// Returns the struct info associated with the Type
+    unsafe fn inner(&self) -> Result<&StructData, String> {
+        match (self.0 as *const StructData).as_ref() {
+            Some(store) => Ok(store),
+            None => Err(String::from("null pointer")),
+        }
+    }
+}
+
+/// Returns the globally unique identifier (GUID) of the struct.
+///
+/// # Safety
+///
+/// This function results in undefined behavior if the passed in `StructType` has been deallocated
+/// by a previous call to [`mun_type_release`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_struct_type_guid(ty: StructInfo, guid: *mut Guid) -> ErrorHandle {
+    let ty = mun_error_try!(ty
+        .inner()
+        .map_err(|e| format!("invalid argument 'ty': {e}")));
+    let guid = try_deref_mut!(guid);
+    *guid = ty.guid;
+    ErrorHandle::default()
+}
+
+/// Returns the type of memory management to apply for the struct.
+///
+/// # Safety
+///
+/// This function results in undefined behavior if the passed in `StructType` has been deallocated
+/// by a previous call to [`mun_type_release`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_struct_type_memory_kind(
+    ty: StructInfo,
+    memory_kind: *mut abi::StructMemoryKind,
+) -> ErrorHandle {
+    let ty = mun_error_try!(ty
+        .inner()
+        .map_err(|e| format!("invalid argument 'ty': {e}")));
+    let memory_kind = try_deref_mut!(memory_kind);
+    *memory_kind = ty.memory_kind;
+    ErrorHandle::default()
+}
+
+/// An array of [`Field`]s.
+///
+/// This is backed by a dynamically allocated array. Ownership is transferred via this struct
+/// and its contents must be destroyed with [`mun_fields_destroy`].
+#[repr(C)]
+#[derive(Copy, Clone)]
+pub struct Fields {
+    pub fields: *const Field,
+    pub count: usize,
+}
+
+/// Retrieves the field with the given name.
+///
+/// The name can be passed as a non nul-terminated string it must be UTF-8 encoded.
+///
+/// # Safety
+///
+/// This function results in undefined behavior if the passed in `Fields` has been deallocated
+/// by a previous call to [`mun_fields_destroy`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_fields_find_by_name(
+    fields: Fields,
+    name: *const c_char,
+    len: usize,
+    has_field: *mut bool,
+    field: *mut Field,
+) -> ErrorHandle {
+    if field.is_null() {
+        return ErrorHandle::new("invalid argument 'field': null pointer");
+    }
+    if name.is_null() {
+        return ErrorHandle::new("invalid argument 'name': null pointer");
+    };
+    let has_field = try_deref_mut!(has_field);
+    let field = try_deref_mut!(field);
+    let name = std::str::from_utf8_unchecked(slice::from_raw_parts(name as *const u8, len));
+
+    *has_field = false;
+
+    if fields.fields.is_null() && fields.count == 0 {
+        return ErrorHandle::default();
+    } else if fields.fields.is_null() {
+        return ErrorHandle::new("invalid argument 'fields': invalid pointer");
+    }
+
+    let fields = slice::from_raw_parts(fields.fields, fields.count);
+    for f in fields {
+        let field_inner = mun_error_try!(f.inner());
+        if field_inner.name == name {
+            *field = *f;
+            *has_field = true;
+            break;
+        }
+    }
+    ErrorHandle::default()
+}
+
+/// Destroys the contents of a [`Fields`] struct.
+///
+/// # Safety
+///
+/// This function results in undefined behavior if the passed in `Fields` has been deallocated
+/// by a previous call to [`mun_fields_destroy`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_fields_destroy(fields: Fields) -> ErrorHandle {
+    if fields.fields.is_null() && fields.count > 0 {
+        return ErrorHandle::new("Fields contains invalid pointer");
+    } else if fields.count > 0 {
+        let _ = Vec::from_raw_parts(fields.fields as *mut Field, fields.count, fields.count);
+    }
+    ErrorHandle::default()
+}
+
+/// Retrieves all the fields of the specified struct type.
+///
+/// # Safety
+///
+/// This function results in undefined behavior if the passed in `StructType` has been deallocated
+/// by a previous call to [`mun_type_release`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_struct_type_fields(
+    ty: StructInfo,
+    fields: *mut Fields,
+) -> ErrorHandle {
+    let inner = mun_error_try!(ty
+        .inner()
+        .map_err(|e| format!("invalid argument 'ty': {e}")));
+    let fields = try_deref_mut!(fields);
+
+    // Get all fields
+    let mut fields_vec = Vec::from_iter(
+        inner
+            .fields
+            .iter()
+            .map(|field| Field((field as *const FieldData).cast(), ty.1)),
+    );
+
+    // Ensures that the length and the capacity are the same
+    fields_vec.shrink_to_fit();
+    debug_assert!(fields_vec.len() == fields_vec.capacity());
+
+    // Transfer ownership over the FFI
+    let fields_vec = ManuallyDrop::new(fields_vec);
+    *fields = Fields {
+        fields: if fields_vec.is_empty() {
+            ptr::null()
+        } else {
+            fields_vec.as_ptr()
+        },
+        count: fields_vec.len(),
+    };
+
+    ErrorHandle::default()
+}
+
+/// Information of a field of a struct [`Type`].
+///
+/// Ownership of this type lies with the [`Type`] that created this instance. As long as the
+/// original type is not released through [`mun_type_release`] this type stays alive.
+#[repr(C)]
+#[derive(Copy, Clone)]
+pub struct Field(*const c_void, *const c_void);
+
+impl Field {
+    /// Returns the store associated with this instance
+    unsafe fn store(&self) -> Result<ManuallyDrop<Arc<TypeDataStore>>, String> {
+        if self.1.is_null() {
+            return Err(String::from("null pointer"));
+        }
+
+        Ok(ManuallyDrop::new(Arc::from_raw(
+            self.1 as *const TypeDataStore,
+        )))
+    }
+
+    /// Returns the field info associated with this instance
+    unsafe fn inner(&self) -> Result<&FieldData, String> {
+        match (self.0 as *const FieldData).as_ref() {
+            Some(info) => Ok(info),
+            None => Err(String::from("null pointer")),
+        }
+    }
+}
+
+/// Returns the name of the field in the parent struct. Ownership of the name is transferred and
+/// must be destroyed with [`mun_string_destroy`]. If this function fails a nullptr is returned.
+///
+/// # Safety
+///
+/// This function results in undefined behavior if the passed in `Field` has been deallocated
+/// by a previous call to [`mun_type_release`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_field_name(field: Field, name: *mut *const c_char) -> ErrorHandle {
+    let inner = mun_error_try!(field
+        .inner()
+        .map_err(|e| format!("invalid argument 'ty': {e}")));
+    let name = try_deref_mut!(name);
+    *name = CString::new(inner.name.clone()).unwrap().into_raw() as *const _;
+    ErrorHandle::default()
+}
+
+/// Returns the type of the field. Ownership of the returned [`Type`] is transferred and must be
+/// released with a call to [`mun_type_release`].
+///
+/// # Safety
+///
+/// This function results in undefined behavior if the passed in `Field` has been deallocated
+/// by a previous call to [`mun_type_release`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_field_type(field: Field, ty: *mut Type) -> ErrorHandle {
+    let inner = mun_error_try!(field
+        .inner()
+        .map_err(|e| format!("invalid argument 'ty': {e}")));
+    let store = mun_error_try!(field
+        .store()
+        .map_err(|e| format!("invalid argument 'ty': {e}")));
+    let ty = try_deref_mut!(ty);
+    *ty = RustType::new_unchecked(inner.type_info, ManuallyDrop::deref(&store).clone()).into();
+    ErrorHandle::default()
+}
+
+/// Returns the offset of the field in bytes from the start of the parent struct.
+///
+/// # Safety
+///
+/// This function results in undefined behavior if the passed in `Field` has been deallocated
+/// by a previous call to [`mun_type_release`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_field_offset(field: Field, offset: *mut usize) -> ErrorHandle {
+    let inner = mun_error_try!(field
+        .inner()
+        .map_err(|e| format!("invalid argument 'ty': {e}")));
+    let offset = try_deref_mut!(offset);
+    *offset = inner.offset as usize;
+    ErrorHandle::default()
+}
+
+#[cfg(test)]
+mod test {
+    use std::ffi::{CStr, CString};
+    use std::{mem::MaybeUninit, ptr, slice};
+
+    use capi_utils::{assert_error_snapshot, assert_getter1, assert_getter3, mun_string_destroy};
+
+    use crate::r#type::ffi::r#struct::mun_fields_find_by_name;
+    use crate::{HasStaticType, StructTypeBuilder};
+
+    use super::{
+        super::{mun_type_kind, mun_type_release, Type, TypeKind},
+        mun_field_name, mun_field_offset, mun_field_type, mun_fields_destroy,
+        mun_struct_type_fields, mun_struct_type_guid, mun_struct_type_memory_kind, Field, Fields,
+        StructInfo,
+    };
+
+    unsafe fn struct_type(ty: Type) -> (Type, StructInfo) {
+        assert_getter1!(mun_type_kind(ty, ty_kind));
+        let pointer_ty = match ty_kind {
+            TypeKind::Struct(p) => p,
+            _ => panic!("invalid type kind for struct"),
+        };
+
+        (ty, pointer_ty)
+    }
+
+    #[test]
+    fn test_mun_struct_type_guid() {
+        let rust_ty = StructTypeBuilder::new("Foo")
+            .add_field("foo", i32::type_info().clone())
+            .finish();
+
+        let guid = rust_ty.as_struct().unwrap().guid().clone();
+        let (ty, struct_ty) = unsafe { struct_type(rust_ty.into()) };
+
+        assert_getter1!(mun_struct_type_guid(struct_ty, ffi_guid));
+        assert_eq!(ffi_guid, guid);
+
+        assert!(unsafe { mun_type_release(ty) }.is_ok());
+    }
+
+    #[test]
+    fn test_mun_struct_type_guid_invalid_null() {
+        let mut guid = MaybeUninit::uninit();
+        assert_error_snapshot!(
+            unsafe {
+                mun_struct_type_guid(StructInfo(ptr::null(), ptr::null()), guid.as_mut_ptr())
+            },
+            @r###""invalid argument \'ty\': null pointer""###
+        );
+
+        let ty = StructTypeBuilder::new("Foo")
+            .add_field("foo", i32::type_info().clone())
+            .finish()
+            .into();
+        let (ty, struct_ty) = unsafe { struct_type(ty) };
+        assert_error_snapshot!(
+            unsafe { mun_struct_type_guid(struct_ty, ptr::null_mut()) },
+            @r###""invalid argument \'guid\': null pointer""###
+        );
+
+        assert!(unsafe { mun_type_release(ty) }.is_ok());
+    }
+
+    #[test]
+    fn test_mun_struct_type_memory_kind() {
+        let rust_ty = StructTypeBuilder::new("Foo")
+            .add_field("foo", i32::type_info().clone())
+            .set_memory_kind(abi::StructMemoryKind::Value)
+            .finish();
+
+        let (ty, struct_ty) = unsafe { struct_type(rust_ty.into()) };
+
+        assert_getter1!(mun_struct_type_memory_kind(struct_ty, memory_kind));
+        assert_eq!(memory_kind, abi::StructMemoryKind::Value);
+
+        assert!(unsafe { mun_type_release(ty) }.is_ok());
+    }
+
+    #[test]
+    fn test_mun_struct_type_memory_kind_invalid_null() {
+        let mut memory_kind = MaybeUninit::uninit();
+        assert_error_snapshot!(unsafe {
+            mun_struct_type_memory_kind(
+                StructInfo(ptr::null(), ptr::null()),
+                memory_kind.as_mut_ptr(),
+            )},
+            @r###""invalid argument \'ty\': null pointer""###
+        );
+
+        let ty = StructTypeBuilder::new("Foo")
+            .add_field("foo", i32::type_info().clone())
+            .finish()
+            .into();
+        let (_ty, struct_ty) = unsafe { struct_type(ty) };
+        assert_error_snapshot!(
+            unsafe { mun_struct_type_memory_kind(struct_ty, ptr::null_mut()) },
+            @r###""invalid argument \'memory_kind\': null pointer""###
+        );
+    }
+
+    #[test]
+    fn test_mun_struct_type_fields() {
+        let i32_type = i32::type_info();
+        let (struct_with_fields, struct_with_fields_struct) = unsafe {
+            struct_type(
+                StructTypeBuilder::new("StructWithFields")
+                    .add_field("foo", i32_type.clone())
+                    .add_field("bar", i32_type.clone())
+                    .finish()
+                    .into(),
+            )
+        };
+
+        assert_getter1!(mun_struct_type_fields(struct_with_fields_struct, fields));
+        assert_eq!(fields.count, 2);
+
+        let fields_slice = unsafe { slice::from_raw_parts(fields.fields, fields.count) };
+
+        assert_getter1!(mun_field_name(fields_slice[0], field_name));
+        assert_getter1!(mun_field_offset(fields_slice[0], field_offset));
+        assert_getter1!(mun_field_type(fields_slice[0], field_type));
+        let field_type = unsafe { field_type.to_owned() }.expect("unable to convert to rust");
+
+        assert_eq!(unsafe { CStr::from_ptr(field_name) }.to_str(), Ok("foo"));
+        assert_eq!(field_offset, 0);
+        assert_eq!(&field_type, i32::type_info());
+
+        unsafe { mun_string_destroy(field_name) };
+        assert!(unsafe { mun_fields_destroy(fields) }.is_ok());
+        assert!(unsafe { mun_type_release(struct_with_fields) }.is_ok());
+    }
+
+    #[test]
+    fn test_mun_struct_type_fields_empty() {
+        let (empty_struct, empty_struct_struct) =
+            unsafe { struct_type(StructTypeBuilder::new("EmptyStruct").finish().into()) };
+
+        assert_getter1!(mun_struct_type_fields(empty_struct_struct, fields));
+        assert_eq!(fields.count, 0);
+        assert!(fields.fields.is_null());
+        assert!(unsafe { mun_fields_destroy(fields) }.is_ok());
+
+        assert!(unsafe { mun_type_release(empty_struct) }.is_ok());
+    }
+
+    #[test]
+    fn test_mun_fields_find_by_name() {
+        let (_foo_type, foo_struct) = unsafe {
+            struct_type(
+                StructTypeBuilder::new("Foo")
+                    .add_field("hello", i32::type_info().clone())
+                    .finish()
+                    .into(),
+            )
+        };
+
+        assert_getter1!(mun_struct_type_fields(foo_struct, fields));
+
+        let hello_str = CString::new("hello").unwrap();
+        let world_str = CString::new("world").unwrap();
+
+        assert_getter3!(mun_fields_find_by_name(
+            fields,
+            hello_str.as_c_str().as_ptr(),
+            hello_str.as_bytes().len(),
+            has_field,
+            _hello_field
+        ));
+        assert!(has_field);
+
+        assert_getter3!(mun_fields_find_by_name(
+            fields,
+            world_str.as_c_str().as_ptr(),
+            world_str.as_bytes().len(),
+            has_field,
+            _world_field
+        ));
+        assert!(!has_field);
+
+        assert_getter3!(mun_fields_find_by_name(
+            Fields {
+                fields: ptr::null(),
+                count: 0
+            },
+            world_str.as_c_str().as_ptr(),
+            world_str.as_bytes().len(),
+            has_field,
+            _world_field
+        ));
+        assert!(!has_field);
+    }
+
+    #[test]
+    fn test_mun_fields_find_by_name_invalid() {
+        let (_empty_struct, empty_struct_struct) =
+            unsafe { struct_type(StructTypeBuilder::new("EmptyStruct").finish().into()) };
+
+        let name = CString::new("hello").unwrap();
+
+        assert_getter1!(mun_struct_type_fields(empty_struct_struct, fields));
+
+        let mut has_field = MaybeUninit::uninit();
+        let mut found_field = MaybeUninit::uninit();
+        assert_error_snapshot!(unsafe {
+            mun_fields_find_by_name(Fields { fields: ptr::null(), count: 10}, name.as_c_str().as_ptr(), name.as_bytes().len(), has_field.as_mut_ptr(), found_field.as_mut_ptr())
+        }, @r###""invalid argument \'fields\': invalid pointer""###);
+        assert_error_snapshot!(unsafe {
+            mun_fields_find_by_name(fields, ptr::null(), 0, has_field.as_mut_ptr(), found_field.as_mut_ptr())
+        }, @r###""invalid argument \'name\': null pointer""###);
+        assert_error_snapshot!(unsafe {
+            mun_fields_find_by_name(fields, name.as_c_str().as_ptr(), name.as_bytes().len(), has_field.as_mut_ptr(), ptr::null_mut())
+        }, @r###""invalid argument \'field\': null pointer""###);
+    }
+
+    #[test]
+    fn test_mun_type_name_offset_type_invalid_null() {
+        let mut name = MaybeUninit::uninit();
+        let mut offset = MaybeUninit::uninit();
+        let mut field_type = MaybeUninit::uninit();
+        assert_error_snapshot!(unsafe {
+            mun_field_name(Field(ptr::null(), ptr::null()), name.as_mut_ptr())
+        }, @r###""invalid argument \'ty\': null pointer""###);
+        assert_error_snapshot!(unsafe {
+            mun_field_offset(Field(ptr::null(), ptr::null()), offset.as_mut_ptr())
+        }, @r###""invalid argument \'ty\': null pointer""###);
+        assert_error_snapshot!(unsafe {
+            mun_field_type(Field(ptr::null(), ptr::null()), field_type.as_mut_ptr())
+        }, @r###""invalid argument \'ty\': null pointer""###);
+
+        let ty = StructTypeBuilder::new("Foo")
+            .add_field("foo", i32::type_info().clone())
+            .finish()
+            .into();
+        let (ty, struct_ty) = unsafe { struct_type(ty) };
+        assert_getter1!(mun_struct_type_fields(struct_ty, fields));
+        assert!(!fields.fields.is_null());
+
+        assert_error_snapshot!(
+            unsafe { mun_field_name(*fields.fields, ptr::null_mut()) },
+            @r###""invalid argument \'name\': null pointer""###
+        );
+        assert_error_snapshot!(
+            unsafe { mun_field_offset(*fields.fields, ptr::null_mut()) },
+            @r###""invalid argument \'offset\': null pointer""###
+        );
+        assert_error_snapshot!(
+            unsafe { mun_field_type(*fields.fields, ptr::null_mut()) },
+            @r###""invalid argument \'ty\': null pointer""###
+        );
+
+        assert!(unsafe { mun_fields_destroy(fields) }.is_ok());
+        assert!(unsafe { mun_type_release(ty) }.is_ok());
+    }
+}
diff --git a/crates/mun_memory/src/type/mod.rs b/crates/mun_memory/src/type/mod.rs
new file mode 100644
index 000000000..2b3bf28ce
--- /dev/null
+++ b/crates/mun_memory/src/type/mod.rs
@@ -0,0 +1,1074 @@
+//! Type information in Mun is stored globally. This allows type information to be stored easily.
+//!
+//! A type is referred to with [`Type`]. A `Type` holds a reference to the underlying data which is
+//! managed by the runtime. `Type`s can be freely created through a [`StructTypeBuilder`], via
+//! [`HasStaticType`] or by querying other types ([`Type::pointer_type`] for instance). Cloning a
+//! [`Type`] is a cheap operation.
+//!
+//! Type information is stored globally on the heap and is freed when no longer referenced by a
+//! `Type`. However, since `Type`s can reference each other a garbage collection algorithm is used
+//! to clean up unreferenced type information. See [`Type::collect_unreferenced_types()`].
+
+pub mod ffi;
+
+use std::{
+    alloc::Layout,
+    borrow::Cow,
+    collections::VecDeque,
+    fmt::{self, Formatter},
+    fmt::{Debug, Display},
+    hash::{Hash, Hasher},
+    ops::Deref,
+    ptr::NonNull,
+    sync::{atomic::AtomicUsize, atomic::Ordering, Arc, Once},
+};
+
+use itertools::izip;
+use once_cell::sync::Lazy;
+use parking_lot::{lock_api::MutexGuard, Mutex, RawMutex, RwLock};
+
+use abi::{self, static_type_map::StaticTypeMap};
+
+use crate::{type_table::TypeTable, TryFromAbiError};
+
+static GLOBAL_TYPE_STORE: Lazy<Arc<TypeDataStore>> = Lazy::new(Default::default);
+
+/// A type store holds a list of interconnected [`TypeData`]s. Type information can contain cycles
+/// so the `TypeData`s refer to each other via pointers. The `TypeDataStore` owns the heap allocated
+/// `TypeData` instances.
+///
+/// By calling [`TypeDataStore::collect_garbage`] types that are no longer referenced by [`Type`]s
+/// are removed.
+#[derive(Default)]
+struct TypeDataStore {
+    types: Mutex<VecDeque<Box<TypeData>>>,
+}
+
+/// Result information after a call to [`TypeDataStore::collect_garbage`].
+#[derive(Clone, Debug)]
+pub struct TypeCollectionStats {
+    pub collected_types: usize,
+    pub remaining_types: usize,
+}
+
+impl TypeDataStore {
+    /// Called to collect types that are no longer externally referenced.
+    pub fn collect_garbage(&self) -> TypeCollectionStats {
+        let mut lock = self.types.lock();
+
+        // Reset all mark flags.
+        let mut queue = VecDeque::new();
+        for ty in lock.iter_mut() {
+            ty.marked = ty.external_references.load(Ordering::Acquire) > 0;
+            queue.push_back(unsafe { NonNull::new_unchecked(Box::as_mut(ty) as *mut TypeData) });
+        }
+
+        // Trace all types
+        while let Some(ty) = queue.pop_back() {
+            let ty = unsafe { ty.as_ref() };
+            match &ty.data {
+                TypeDataKind::Struct(s) => {
+                    for field in s.fields.iter() {
+                        let mut field_ty = field.type_info;
+                        let field_ty = unsafe { field_ty.as_mut() };
+                        if !field_ty.marked {
+                            field_ty.marked = true;
+                            queue.push_back(field.type_info);
+                        }
+                    }
+                }
+                TypeDataKind::Pointer(p) => {
+                    let mut pointee = p.pointee;
+                    let pointee = unsafe { pointee.as_mut() };
+                    if !pointee.marked {
+                        pointee.marked = true;
+                        queue.push_back(p.pointee);
+                    }
+                }
+                TypeDataKind::Primitive(_) | TypeDataKind::Uninitialized => {}
+            }
+
+            // Iterate over the indirections. This is an interesting case safety wise, because at
+            // this very moment another thread might be accessing this as well. However this is safe
+            // because we use `allocate_into` to allocate the values.
+            for indirection in [&ty.mutable_pointer_type, &ty.immutable_pointer_type] {
+                let read_lock = indirection.read();
+                if let &Some(mut indirection_ref) = read_lock.deref() {
+                    let reference = unsafe { indirection_ref.as_mut() };
+                    if !reference.marked {
+                        reference.marked = true;
+                        queue.push_back(indirection_ref);
+                    }
+                }
+            }
+        }
+
+        // Iterate over all objects and remove the ones that are no longer referenced
+        let mut types_removed = 0;
+        let mut index = 0;
+        while index < lock.len() {
+            let ty = &(&*lock)[index];
+            if !ty.marked {
+                lock.swap_remove_back(index);
+                types_removed += 1;
+            } else {
+                index += 1;
+            }
+        }
+
+        TypeCollectionStats {
+            collected_types: types_removed,
+            remaining_types: lock.len(),
+        }
+    }
+
+    /// Tries to convert multiple [`abi::TypeDefinition`] to internal type representations. If
+    /// the conversion succeeds an updated [`TypeTable`] is returned
+    pub fn try_from_abi<'abi>(
+        self: &Arc<Self>,
+        definitions: impl Iterator<Item = &'abi abi::TypeDefinition<'abi>>,
+        mut type_table: TypeTable,
+    ) -> Result<(TypeTable, Vec<Type>), TryFromAbiError<'abi>> {
+        // Acquire a lock in the type entries
+        let mut entries = self.types.lock();
+
+        // Create uninitialized types for all the definitions
+        let mut types = Vec::new();
+        let mut definition_and_type = Vec::with_capacity(definitions.size_hint().0);
+        for type_def in definitions {
+            let ty = self.allocate_inner(
+                type_def.name().to_owned(),
+                Layout::from_size_align(type_def.size_in_bytes(), type_def.alignment())
+                    .expect("invalid abi type definition layout"),
+                TypeDataKind::Uninitialized,
+                &mut entries,
+            );
+            type_table.insert_concrete_type(*type_def.as_concrete(), ty.clone());
+            types.push(ty.clone());
+            definition_and_type.push((type_def, ty));
+        }
+
+        // Next, initialize the types.
+        for (type_def, mut ty) in definition_and_type {
+            // Safety: we are modifying the inner data of the type here. At this point this is safe
+            // because the type cannot be used by anything else yet.
+            let inner_ty = unsafe { ty.inner.as_mut() };
+            let type_data = match &type_def.data {
+                abi::TypeDefinitionData::Struct(s) => {
+                    StructData::try_from_abi(s, &type_table)?.into()
+                }
+            };
+            inner_ty.data = type_data;
+        }
+
+        Ok((type_table, types))
+    }
+
+    fn allocate_inner(
+        self: &Arc<Self>,
+        name: impl Into<String>,
+        layout: Layout,
+        data: TypeDataKind,
+        entries: &mut MutexGuard<'_, RawMutex, VecDeque<Box<TypeData>>>,
+    ) -> Type {
+        entries.push_back(Box::new(TypeData {
+            name: name.into(),
+            layout,
+            data,
+            external_references: AtomicUsize::new(0),
+            immutable_pointer_type: Default::default(),
+            mutable_pointer_type: Default::default(),
+            marked: false,
+        }));
+
+        // Safety: get a TypeInner with a 'static lifetime. This is safe because of the nature of
+        // `Type`. The `Type` struct ensures that the pointed to value is never destructed as long
+        // as it lives.
+        let entry = unsafe {
+            NonNull::new_unchecked(
+                entries.back().expect("didnt insert").deref() as *const TypeData as *mut _,
+            )
+        };
+
+        // Safety: this operation is safe, because we currently own the instance and the lock.
+        unsafe { Type::new_unchecked(entry, self.clone()) }
+    }
+
+    /// Special case allocation method that acquires the creation lock and with the lock held,
+    /// writes the result to a location in memory. This ensure that the garbage collector sees
+    /// both the allocation and the store as one atomic operation. This is required when allocating
+    /// indirections because if the two operations would be seperate the GC might deallocate the
+    /// type before the type is assigned.
+    fn allocate_into(
+        self: &Arc<Self>,
+        name: impl Into<String>,
+        layout: Layout,
+        data: TypeDataKind,
+        into: &mut NonNull<TypeData>,
+    ) -> Type {
+        let mut entries = self.types.lock();
+        let ty = self.allocate_inner(name, layout, data, &mut entries);
+        *into = ty.inner;
+        ty
+    }
+
+    /// Allocates a new type instance
+    pub fn allocate(
+        self: &Arc<Self>,
+        name: impl Into<String>,
+        layout: Layout,
+        data: TypeDataKind,
+    ) -> Type {
+        let mut entries = self.types.lock();
+        self.allocate_inner(name, layout, data, &mut entries)
+    }
+}
+
+/// A reference to internally stored type information. A `Type` can be used to query information,
+/// construct other types, or store type information for later use.
+pub struct Type {
+    inner: NonNull<TypeData>,
+
+    /// A [`Type`] holds a strong reference to its data store. This ensure that the data is never
+    /// deleted before this instance is destroyed.
+    ///
+    /// This has to be here because we store [`Type`] instances globally both in Rust and
+    /// potentially als over FFI. Since the static destruction order is not completely guarenteed
+    /// the store might be deallocated before the last type is deallocated. Keeping a reference to
+    /// the store in this instance ensures that the data is kept alive until the last `Type` is
+    /// dropped.
+    store: Arc<TypeDataStore>,
+}
+
+impl Type {
+    /// Constructs a new instance from a pointer and the store it belongs to.
+    ///
+    /// # Safety
+    ///
+    /// The pointer pointed to by `inner` might be invalid, in which case this method will cause
+    /// undefined behavior.
+    unsafe fn new_unchecked(mut inner: NonNull<TypeData>, store: Arc<TypeDataStore>) -> Self {
+        // Increment the external reference count
+        inner
+            .as_mut()
+            .external_references
+            .fetch_add(1, Ordering::AcqRel);
+
+        Self { inner, store }
+    }
+}
+
+// Types can safely be used across multiple threads.
+unsafe impl Send for Type {}
+unsafe impl Sync for Type {}
+
+impl Debug for Type {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        std::fmt::Display::fmt(self, f)
+    }
+}
+
+impl Display for Type {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        std::fmt::Display::fmt(&self.name(), f)
+    }
+}
+
+impl Clone for Type {
+    fn clone(&self) -> Self {
+        self.inner()
+            .external_references
+            .fetch_add(1, Ordering::AcqRel);
+
+        Self {
+            store: self.store.clone(),
+            inner: self.inner,
+        }
+    }
+}
+
+impl Drop for Type {
+    fn drop(&mut self) {
+        self.inner()
+            .external_references
+            .fetch_sub(1, Ordering::Release);
+    }
+}
+
+impl PartialEq for Type {
+    fn eq(&self, other: &Self) -> bool {
+        self.inner() == other.inner()
+    }
+}
+
+impl Eq for Type {}
+impl Hash for Type {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        self.inner().hash(state)
+    }
+}
+
+/// Stores type information for a particular type as well as references to related types.
+pub struct TypeData {
+    /// Type name
+    name: String,
+
+    /// The memory layout of the type
+    layout: Layout,
+
+    /// Type group
+    data: TypeDataKind,
+
+    /// Holds the number of external (non-cyclic) references. Basically the number of [`Type`]
+    /// instances pointing to this instance.
+    ///
+    /// Note that if this ever reaches zero is doesn't mean it's no longer used because it can still
+    /// be referenced by other types.
+    external_references: AtomicUsize,
+
+    /// The type of an immutable pointer to this type
+    immutable_pointer_type: RwLock<Option<NonNull<TypeData>>>,
+
+    /// The type of a mutable pointer to this type
+    mutable_pointer_type: RwLock<Option<NonNull<TypeData>>>,
+
+    /// True if this type has been marked as alive (only valid for GC).
+    marked: bool,
+}
+
+impl TypeData {
+    /// Returns the type that represents a pointer to this type
+    fn pointer_type(&self, mutable: bool, store: &Arc<TypeDataStore>) -> Type {
+        let cache_key = if mutable {
+            &self.mutable_pointer_type
+        } else {
+            &self.immutable_pointer_type
+        };
+
+        {
+            let read_lock = cache_key.read();
+
+            // Fast path, the type already exists, return it immediately.
+            if let Some(ty) = read_lock.deref().as_ref() {
+                return Type {
+                    inner: *ty,
+                    store: store.clone(),
+                };
+            }
+        }
+
+        let mut write_lock = cache_key.write();
+
+        // Recheck if another thread acquired the write lock in the mean time
+        if let Some(ty) = write_lock.deref() {
+            return Type {
+                inner: *ty,
+                store: store.clone(),
+            };
+        }
+
+        // Otherwise create the type and store it
+        let name = format!("*{} {}", if mutable { "mut" } else { "const" }, self.name);
+
+        *write_lock = Some(NonNull::dangling());
+        let ty = store.allocate_into(
+            name,
+            Layout::new::<*const std::ffi::c_void>(),
+            PointerData {
+                pointee: self.into(),
+                mutable,
+            }
+            .into(),
+            // Safe because we ensure that write_lock is Some above.
+            unsafe { write_lock.as_mut().unwrap_unchecked() },
+        );
+
+        ty
+    }
+}
+
+impl PartialEq for TypeData {
+    fn eq(&self, other: &Self) -> bool {
+        self.name == other.name && self.layout == other.layout && self.data == other.data
+    }
+}
+
+impl Eq for TypeData {}
+
+unsafe impl Send for TypeData {}
+unsafe impl Sync for TypeData {}
+
+/// A linked version of [`mun_abi::TypeInfoData`] that has resolved all occurrences of `TypeId` with `TypeInfo`.
+#[derive(Clone, Debug, Eq, PartialEq, Hash)]
+enum TypeDataKind {
+    /// Primitive types (i.e. `()`, `bool`, `float`, `int`, etc.)
+    Primitive(abi::Guid),
+    /// Struct types (i.e. record, tuple, or unit structs)
+    Struct(StructData),
+    /// A pointer to another type
+    Pointer(PointerData),
+    /// Indicates that the type has been allocated but it has not yet been initialized,
+    /// this indicates that it still needs to be properly initialized.
+    Uninitialized,
+}
+
+#[derive(Copy, Clone)]
+pub enum TypeKind<'t> {
+    /// Primitive types (i.e. `()`, `bool`, `float`, `int`, etc.)
+    Primitive(&'t abi::Guid),
+    /// Struct types (i.e. record, tuple, or unit structs)
+    Struct(StructType<'t>),
+    /// A pointer to another type
+    Pointer(PointerType<'t>),
+}
+
+/// A linked version of [`mun_abi::StructInfo`] that has resolved all occurrences of `TypeId` with `TypeInfo`.
+#[derive(Clone, Debug)]
+struct StructData {
+    /// The unique identifier of this struct
+    pub guid: abi::Guid,
+    /// Struct fields
+    pub fields: Vec<FieldData>,
+    /// Struct memory kind
+    pub memory_kind: abi::StructMemoryKind,
+}
+
+/// Reference information of a struct
+#[repr(C)]
+#[derive(Copy, Clone)]
+pub struct StructType<'t> {
+    inner: &'t StructData,
+    store: &'t Arc<TypeDataStore>,
+}
+
+impl<'t> StructType<'t> {
+    /// Returns the unique identifier of this struct
+    pub fn guid<'s>(&'s self) -> &'t abi::Guid
+    where
+        't: 's,
+    {
+        &self.inner.guid
+    }
+
+    /// Returns the memory type of this struct
+    pub fn memory_kind(&self) -> abi::StructMemoryKind {
+        self.inner.memory_kind
+    }
+
+    /// Returns true if this struct is a value struct. Value structs are passed by value and are not
+    /// allocated by the garbage collector.
+    pub fn is_value_struct(&self) -> bool {
+        self.memory_kind() == abi::StructMemoryKind::Value
+    }
+
+    /// Returns true if this struct is a garbage collected struct.
+    pub fn is_gc_struct(&self) -> bool {
+        self.memory_kind() == abi::StructMemoryKind::Gc
+    }
+
+    /// Returns an iterator over all fields
+    pub fn fields(&self) -> Fields<'t> {
+        Fields {
+            inner: self.inner,
+            store: self.store,
+        }
+    }
+}
+
+/// A collection of fields of a struct
+#[derive(Copy, Clone)]
+pub struct Fields<'t> {
+    inner: &'t StructData,
+    store: &'t Arc<TypeDataStore>,
+}
+
+impl<'t> Fields<'t> {
+    /// Returns the number of fields in the struct
+    pub fn len(&self) -> usize {
+        self.inner.fields.len()
+    }
+
+    /// Returns the field at the given index, or `None` if `index` exceeds the number of fields.
+    pub fn get(&self, index: usize) -> Option<Field<'t>> {
+        self.inner.fields.get(index).map(|field| Field {
+            inner: field,
+            store: self.store,
+        })
+    }
+
+    /// Returns the field with the given name, or `None` if no such field exists.
+    pub fn find_by_name(&self, name: impl AsRef<str>) -> Option<Field<'t>> {
+        let field_name = name.as_ref();
+        self.iter().find(|field| field.name() == field_name)
+    }
+
+    /// Returns an iterator over all fields
+    pub fn iter(&self) -> FieldsIterator<'t> {
+        FieldsIterator {
+            iter: self.inner.fields.iter(),
+            store: self.store,
+        }
+    }
+}
+
+impl<'t> IntoIterator for Fields<'t> {
+    type Item = Field<'t>;
+    type IntoIter = FieldsIterator<'t>;
+
+    fn into_iter(self) -> Self::IntoIter {
+        FieldsIterator {
+            iter: self.inner.fields.iter(),
+            store: self.store,
+        }
+    }
+}
+
+pub struct FieldsIterator<'t> {
+    iter: std::slice::Iter<'t, FieldData>,
+    store: &'t Arc<TypeDataStore>,
+}
+
+impl<'t> Iterator for FieldsIterator<'t> {
+    type Item = Field<'t>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        self.iter.next().map(|field| Field {
+            inner: field,
+            store: self.store,
+        })
+    }
+}
+
+/// A linked version of [`mun_abi::PointerInfo`] that has resolved all occurrences of `TypeId` with `TypeInfo`.
+#[derive(Clone, Debug, Eq, PartialEq, Hash)]
+struct PointerData {
+    /// The type to which is pointed
+    pub pointee: NonNull<TypeData>,
+    /// Whether or not the pointer is mutable
+    pub mutable: bool,
+}
+
+/// Reference information of a pointer
+#[derive(Copy, Clone)]
+pub struct PointerType<'t> {
+    inner: &'t PointerData,
+    store: &'t Arc<TypeDataStore>,
+}
+
+impl<'t> PointerType<'t> {
+    /// Returns the type to which this pointer points
+    pub fn pointee(&self) -> Type {
+        // Safety: this operation is safe due to the lifetime constraints on this type
+        unsafe { Type::new_unchecked(self.inner.pointee, self.store.clone()) }
+    }
+
+    /// Returns true if this is a mutable pointer type
+    pub fn is_mutable(&self) -> bool {
+        self.inner.mutable
+    }
+
+    /// Returns true if this is a immutable pointer type
+    pub fn is_immutable(&self) -> bool {
+        !self.inner.mutable
+    }
+}
+
+impl From<StructData> for TypeDataKind {
+    fn from(s: StructData) -> Self {
+        TypeDataKind::Struct(s)
+    }
+}
+
+impl From<PointerData> for TypeDataKind {
+    fn from(p: PointerData) -> Self {
+        TypeDataKind::Pointer(p)
+    }
+}
+
+impl Hash for TypeData {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        Hash::hash(&self.data, state);
+    }
+}
+
+impl Hash for StructData {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        self.guid.hash(state)
+    }
+}
+
+impl PartialEq for StructData {
+    fn eq(&self, other: &Self) -> bool {
+        self.guid == other.guid
+    }
+}
+impl Eq for StructData {}
+
+impl Type {
+    /// Collects all data related to types that are no longer referenced by a [`Type`]. Returns
+    /// the number of types that were removed.
+    pub fn collect_unreferenced_type_data() -> TypeCollectionStats {
+        GLOBAL_TYPE_STORE.collect_garbage()
+    }
+
+    /// Constructs a new struct type
+    pub fn new_struct(
+        name: impl Into<String>,
+        layout: Layout,
+        guid: abi::Guid,
+        fields: impl IntoIterator<Item = (String, Type, u16)>,
+        memory_kind: abi::StructMemoryKind,
+    ) -> Type {
+        GLOBAL_TYPE_STORE.allocate(
+            name,
+            layout,
+            StructData {
+                guid,
+                fields: fields
+                    .into_iter()
+                    .map(|(name, ty, offset)| FieldData {
+                        name,
+                        type_info: ty.inner,
+                        offset,
+                    })
+                    .collect(),
+                memory_kind,
+            }
+            .into(),
+        )
+    }
+
+    /// Returns a reference to the [`TypeInner`]
+    fn inner(&self) -> &TypeData {
+        // Safety: taking the reference is always ok because the garbage collector ensures that as
+        // long as self (Type) exists the inner stays alive.
+        unsafe { self.inner.as_ref() }
+    }
+
+    /// Returns the name of the type
+    pub fn name(&self) -> &str {
+        self.inner().name.as_str()
+    }
+
+    /// Returns the type layout
+    pub fn layout(&self) -> Layout {
+        self.inner().layout
+    }
+
+    /// Returns true if this instance represents the TypeInfo of the given type.
+    ///
+    /// ```rust
+    /// # use mun_memory::HasStaticType;
+    /// assert!(i64::type_info().equals::<i64>());
+    /// assert!(!i64::type_info().equals::<f64>())
+    /// ```
+    pub fn equals<T: HasStaticType>(&self) -> bool {
+        T::type_info() == self
+    }
+
+    /// Returns whether this is a fundamental type.
+    pub fn is_primitive(&self) -> bool {
+        matches!(self.kind(), TypeKind::Primitive(_))
+    }
+
+    /// Returns whether this is a struct type.
+    pub fn is_struct(&self) -> bool {
+        matches!(self.kind(), TypeKind::Struct(_))
+    }
+
+    /// Returns whether this is a pointer type.
+    pub fn is_pointer(&self) -> bool {
+        matches!(self.kind(), TypeKind::Pointer(_))
+    }
+
+    /// Returns the kind of the type
+    pub fn kind(&self) -> TypeKind<'_> {
+        match &self.inner().data {
+            TypeDataKind::Primitive(guid) => TypeKind::Primitive(guid),
+            TypeDataKind::Struct(s) => TypeKind::Struct(StructType {
+                inner: s,
+                store: &self.store,
+            }),
+            TypeDataKind::Pointer(p) => TypeKind::Pointer(PointerType {
+                inner: p,
+                store: &self.store,
+            }),
+            TypeDataKind::Uninitialized => {
+                unreachable!("should never be able to query the kind of an uninitialized type")
+            }
+        }
+    }
+
+    /// Returns true if this type is a concrete type. This is the case for any type that doesn't
+    /// refer to another type like a pointer.
+    pub fn is_concrete(&self) -> bool {
+        match self.kind() {
+            TypeKind::Primitive(_) | TypeKind::Struct(_) => true,
+            TypeKind::Pointer(_) => false,
+        }
+    }
+
+    /// Returns the GUID associated with this instance if this instance represents a concrete type.
+    pub fn as_concrete(&self) -> Option<&abi::Guid> {
+        match self.kind() {
+            TypeKind::Primitive(g) => Some(g),
+            TypeKind::Struct(s) => Some(s.guid()),
+            TypeKind::Pointer(_) => None,
+        }
+    }
+
+    /// Retrieves the type's struct information, if available.
+    pub fn as_struct(&self) -> Option<StructType<'_>> {
+        if let TypeKind::Struct(s) = self.kind() {
+            Some(s)
+        } else {
+            None
+        }
+    }
+
+    /// Retrieves the type's pointer information, if available.
+    pub fn as_pointer(&self) -> Option<PointerType<'_>> {
+        if let TypeKind::Pointer(p) = self.kind() {
+            Some(p)
+        } else {
+            None
+        }
+    }
+
+    /// Returns whether the type is allocated on the stack.
+    pub fn is_stack_allocated(&self) -> bool {
+        match self.kind() {
+            TypeKind::Primitive(_) | TypeKind::Pointer(_) => true,
+            TypeKind::Struct(s) => s.is_value_struct(),
+        }
+    }
+
+    /// Tries to convert multiple [`abi::TypeDefinition`] to internal type representations. If
+    /// the conversion succeeds an updated [`TypeTable`] is returned
+    pub fn try_from_abi<'abi>(
+        type_info: impl IntoIterator<Item = &'abi abi::TypeDefinition<'abi>>,
+        type_table: TypeTable,
+    ) -> Result<(TypeTable, Vec<Type>), TryFromAbiError<'abi>> {
+        GLOBAL_TYPE_STORE.try_from_abi(type_info.into_iter(), type_table)
+    }
+
+    /// Returns the type that represents a pointer to this type
+    pub fn pointer_type(&self, mutable: bool) -> Type {
+        self.inner().pointer_type(mutable, &self.store)
+    }
+
+    /// Consumes the `Type`, returning a wrapped raw pointer.
+    ///
+    /// After calling this function, the caller is responsible for the memory previously managed by
+    /// the `Type`. The easiest way to do this is to convert the raw pointer back into a `Type` with
+    /// the [`Type::from_raw`] function, allowing the `Type` destructor to perform the cleanup.
+    pub fn into_raw(ty: Type) -> *const std::ffi::c_void {
+        ty.inner.as_ptr().cast()
+    }
+
+    /// Constructs a box from a raw pointer.
+    ///
+    /// After calling this function, the raw pointer is owned by the resulting `Type`. Specifically,
+    /// the `Type` destructor will ensure the memory previously retained by the `raw` will be
+    /// properly cleaned up. For this to be safe, the passed in `raw` pointer must have been
+    /// previously returned by [`Type::into_raw`].
+    ///
+    /// This function must also not be called as part of static deinitialization as that may cause
+    /// undefined behavior in the underlying implementation. Therefor passing the raw pointer over
+    /// FFI might not be safe. Instead, wrap the `Type` in an `Arc` or a `Box` and use that on the
+    /// FFI boundary.
+    ///
+    /// # Safety
+    ///
+    /// This function is unsafe because improper use may lead to memory problems. For example, a
+    /// double-free may occur if the function is called twice on the same raw pointer.
+    pub unsafe fn from_raw(raw: *const std::ffi::c_void) -> Type {
+        Type {
+            inner: NonNull::new(raw as *mut _).expect("invalid raw pointer"),
+            store: GLOBAL_TYPE_STORE.clone(),
+        }
+    }
+}
+
+impl StructData {
+    /// Tries to convert from an `abi::StructInfo`.
+    fn try_from_abi<'abi>(
+        struct_info: &'abi abi::StructDefinition<'abi>,
+        type_table: &TypeTable,
+    ) -> Result<StructData, TryFromAbiError<'abi>> {
+        let fields: Result<Vec<FieldData>, TryFromAbiError> = izip!(
+            struct_info.field_names(),
+            struct_info.field_types(),
+            struct_info.field_offsets()
+        )
+        .map(|(name, type_id, offset)| {
+            type_table
+                .find_type_info_by_id(type_id)
+                .ok_or_else(|| TryFromAbiError::UnknownTypeId(type_id.clone()))
+                .map(|type_info| FieldData {
+                    name: name.to_owned(),
+                    type_info: type_info.inner,
+                    offset: *offset,
+                })
+        })
+        .collect();
+
+        fields.map(|fields| StructData {
+            guid: struct_info.guid,
+            fields,
+            memory_kind: struct_info.memory_kind,
+        })
+    }
+}
+
+/// A linked version of a struct field.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct FieldData {
+    /// The field's name
+    pub name: String,
+    /// The field's type
+    pub type_info: NonNull<TypeData>,
+    /// The field's offset
+    pub offset: u16,
+    // TODO: Field accessibility levels
+    // const MunPrivacy_t *field_privacies,
+}
+
+#[derive(Copy, Clone)]
+pub struct Field<'t> {
+    inner: &'t FieldData,
+    store: &'t Arc<TypeDataStore>,
+}
+
+impl<'t> Field<'t> {
+    /// Returns the name of the field
+    pub fn name<'s>(&'s self) -> &'t str
+    where
+        't: 's,
+    {
+        self.inner.name.as_str()
+    }
+
+    /// Returns the type of the field
+    pub fn ty(&self) -> Type {
+        // Safety: this operation is safe due to the lifetime constraints on this type
+        unsafe { Type::new_unchecked(self.inner.type_info, self.store.clone()) }
+    }
+
+    /// Returns the offset of the field from the start of the parent struct
+    pub fn offset(&self) -> usize {
+        self.inner.offset as _
+    }
+}
+
+/// A helper struct to create a struct type.
+pub struct StructTypeBuilder {
+    /// The name of the struct type
+    name: String,
+
+    /// The type of memory management for this struct
+    memory_kind: abi::StructMemoryKind,
+
+    /// The fields of the struct
+    fields: Vec<(String, Type, usize)>,
+
+    /// Layout of the struct.
+    layout: Layout,
+
+    /// Optional explicit type of the struct
+    guid: Option<abi::Guid>,
+}
+
+impl StructTypeBuilder {
+    pub fn new(name: impl Into<String>) -> Self {
+        Self {
+            name: name.into(),
+            memory_kind: abi::StructMemoryKind::Gc,
+            fields: Vec::new(),
+            layout: Layout::from_size_align(0, 1).expect("invalid default layout"),
+            guid: None,
+        }
+    }
+
+    /// Sets the memory kind of the struct
+    pub fn set_memory_kind(mut self, kind: abi::StructMemoryKind) -> Self {
+        self.memory_kind = kind;
+        self
+    }
+
+    /// Adds a field to the struct
+    pub fn add_field(mut self, name: impl Into<String>, ty: Type) -> Self {
+        let field_layout = if ty.is_stack_allocated() {
+            ty.layout()
+        } else {
+            Layout::new::<std::ffi::c_void>()
+        };
+
+        let (new_layout, offset) = self
+            .layout
+            .extend(field_layout)
+            .expect("cannot extend struct layout");
+        self.fields.push((name.into(), ty, offset));
+        self.layout = new_layout;
+        self
+    }
+
+    /// Adds a collection of fields to the struct
+    pub fn add_fields<N: Into<String>>(
+        mut self,
+        iter: impl IntoIterator<Item = (N, Type)>,
+    ) -> Self {
+        for (name, ty) in iter.into_iter() {
+            self = self.add_field(name.into(), ty);
+        }
+        self
+    }
+
+    /// Finishes building the struct returning the corresponding [`Type`].
+    pub fn finish(self) -> Type {
+        let guid = if let Some(guid) = self.guid {
+            guid
+        } else {
+            let guid_string = build_struct_guid_string(
+                &self.name,
+                self.fields
+                    .iter()
+                    .map(|(name, ty, offset)| (name, Cow::Borrowed(ty), *offset)),
+            );
+            abi::Guid::from_str(&guid_string)
+        };
+
+        GLOBAL_TYPE_STORE.allocate(
+            self.name,
+            self.layout,
+            StructData {
+                guid,
+                fields: Vec::from_iter(self.fields.into_iter().map(|(name, ty, offset)| {
+                    FieldData {
+                        name,
+                        type_info: ty.inner,
+                        offset: offset.try_into().expect("offset is too large!"),
+                    }
+                })),
+                memory_kind: self.memory_kind,
+            }
+            .into(),
+        )
+    }
+}
+
+/// Constructs a string that unique identifies a struct with the given name and fields.
+fn build_struct_guid_string<'t, N: AsRef<str> + 't>(
+    name: &str,
+    fields: impl Iterator<Item = (N, Cow<'t, Type>, usize)>,
+) -> String {
+    let fields: Vec<String> = fields
+        .map(|(name, ty, _offset)| {
+            let ty_string = build_type_guid_string(ty.as_ref());
+            format!("{}: {}", name.as_ref(), ty_string)
+        })
+        .collect();
+
+    format!(
+        "struct {name}{{{fields}}}",
+        name = name,
+        fields = fields.join(",")
+    )
+}
+
+/// Constructs a string that unique identifies the specified type.
+fn build_type_guid_string(ty: &Type) -> String {
+    match ty.kind() {
+        TypeKind::Struct(s) => {
+            if s.is_gc_struct() {
+                format!("struct {}", ty.name())
+            } else {
+                build_struct_guid_string(
+                    ty.name(),
+                    s.fields()
+                        .iter()
+                        .map(|f| (f.name(), Cow::Owned(f.ty()), f.offset())),
+                )
+            }
+        }
+        TypeKind::Primitive(_) | TypeKind::Pointer(_) => ty.name().to_owned(),
+    }
+}
+
+/// A trait that defines static type information for types that can provide it.
+pub trait HasStaticType {
+    fn type_info() -> &'static Type;
+}
+
+macro_rules! impl_primitive_type {
+    ($($ty:ty),+) => {
+        $(
+            impl HasStaticType for $ty {
+                fn type_info() -> &'static Type {
+                    static TYPE_INFO: once_cell::sync::OnceCell<Type> = once_cell::sync::OnceCell::new();
+                    TYPE_INFO.get_or_init(|| {
+                         GLOBAL_TYPE_STORE.allocate(
+                             <$ty as abi::PrimitiveType>::name(),
+                             Layout::new::<$ty>(),
+                             TypeDataKind::Primitive(<$ty as abi::PrimitiveType>::guid().clone())
+                         )
+                    })
+                }
+            }
+        )+
+    }
+}
+
+impl_primitive_type!(
+    i8,
+    i16,
+    i32,
+    i64,
+    i128,
+    isize,
+    u8,
+    u16,
+    u32,
+    u64,
+    u128,
+    usize,
+    f32,
+    f64,
+    bool,
+    (),
+    std::ffi::c_void
+);
+
+/// Every type that has at least a type name also has a valid pointer type name
+impl<T: HasStaticType + 'static> HasStaticType for *mut T {
+    fn type_info() -> &'static Type {
+        static mut VALUE: Option<StaticTypeMap<Type>> = None;
+        static INIT: Once = Once::new();
+
+        let map = unsafe {
+            INIT.call_once(|| {
+                VALUE = Some(StaticTypeMap::default());
+            });
+            VALUE.as_ref().unwrap()
+        };
+
+        map.call_once::<T, _>(|| T::type_info().pointer_type(true))
+    }
+}
+
+/// Every type that has at least a type name also has a valid pointer type name
+impl<T: HasStaticType + 'static> HasStaticType for *const T {
+    fn type_info() -> &'static Type {
+        static mut VALUE: Option<StaticTypeMap<Type>> = None;
+        static INIT: Once = Once::new();
+
+        let map = unsafe {
+            INIT.call_once(|| {
+                VALUE = Some(StaticTypeMap::default());
+            });
+            VALUE.as_ref().unwrap()
+        };
+
+        map.call_once::<T, _>(|| T::type_info().pointer_type(false))
+    }
+}
diff --git a/crates/mun_memory/src/type_info.rs b/crates/mun_memory/src/type_info.rs
deleted file mode 100644
index 11f4e7646..000000000
--- a/crates/mun_memory/src/type_info.rs
+++ /dev/null
@@ -1,434 +0,0 @@
-use std::{
-    alloc::Layout,
-    fmt::{self, Formatter},
-    hash::{Hash, Hasher},
-    sync::Arc,
-    sync::{Once, Weak},
-};
-
-use itertools::izip;
-use parking_lot::RwLock;
-
-use abi::{static_type_map::StaticTypeMap, Guid, StructMemoryKind};
-
-use crate::{type_table::TypeTable, TryFromAbiError, TypeFields};
-
-/// A linked version of [`mun_abi::TypeInfo`] that has resolved all occurrences of `TypeId` with `TypeInfo`.
-#[derive(Debug)]
-pub struct TypeInfo {
-    /// Type name
-    pub name: String,
-    /// The memory layout of the type
-    pub layout: Layout,
-    /// Type group
-    pub data: TypeInfoData,
-
-    /// The type of an immutable pointer to this type
-    immutable_pointer_type: RwLock<Weak<TypeInfo>>,
-
-    /// The type of a mutable pointer to this type
-    mutable_pointer_type: RwLock<Weak<TypeInfo>>,
-}
-
-impl PartialEq for TypeInfo {
-    fn eq(&self, other: &Self) -> bool {
-        self.name == other.name && self.layout == other.layout && self.data == other.data
-    }
-}
-
-impl Eq for TypeInfo {}
-
-/// A linked version of [`mun_abi::TypeInfoData`] that has resolved all occurrences of `TypeId` with `TypeInfo`.
-#[repr(u8)]
-#[derive(Clone, Debug, Eq, PartialEq, Hash)]
-pub enum TypeInfoData {
-    /// Primitive types (i.e. `()`, `bool`, `float`, `int`, etc.)
-    Primitive(Guid),
-    /// Struct types (i.e. record, tuple, or unit structs)
-    Struct(StructInfo),
-    /// A pointer to another type
-    Pointer(PointerInfo),
-}
-
-/// A linked version of [`mun_abi::StructInfo`] that has resolved all occurrences of `TypeId` with `TypeInfo`.
-#[derive(Clone, Debug)]
-pub struct StructInfo {
-    /// The unique identifier of this struct
-    pub guid: Guid,
-    /// Struct fields
-    pub fields: Vec<FieldInfo>,
-    /// Struct memory kind
-    pub memory_kind: abi::StructMemoryKind,
-}
-
-/// A linked version of [`mun_abi::PointerInfo`] that has resolved all occurances of `TypeId` with `TypeInfo`.
-#[derive(Clone, Debug, Eq, PartialEq, Hash)]
-pub struct PointerInfo {
-    /// The type to which is pointed
-    pub pointee: Arc<TypeInfo>,
-    /// Whether or not the pointer is mutable
-    pub mutable: bool,
-}
-
-impl From<StructInfo> for TypeInfoData {
-    fn from(s: StructInfo) -> Self {
-        TypeInfoData::Struct(s)
-    }
-}
-
-impl From<PointerInfo> for TypeInfoData {
-    fn from(p: PointerInfo) -> Self {
-        TypeInfoData::Pointer(p)
-    }
-}
-
-impl fmt::Display for TypeInfo {
-    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
-        write!(f, "{}", self.name)
-    }
-}
-
-impl TypeFields for Arc<TypeInfo> {
-    fn fields(&self) -> &[FieldInfo] {
-        if let TypeInfoData::Struct(s) = &self.data {
-            &s.fields
-        } else {
-            &[]
-        }
-    }
-}
-
-impl Hash for TypeInfo {
-    fn hash<H: Hasher>(&self, state: &mut H) {
-        self.data.hash(state)
-    }
-}
-
-impl Hash for StructInfo {
-    fn hash<H: Hasher>(&self, state: &mut H) {
-        self.guid.hash(state)
-    }
-}
-
-impl PartialEq for StructInfo {
-    fn eq(&self, other: &Self) -> bool {
-        self.guid == other.guid
-    }
-}
-impl Eq for StructInfo {}
-
-impl TypeInfo {
-    /// Construct a new pointer type
-    pub fn new_pointer(pointee: Arc<TypeInfo>, mutable: bool) -> Arc<TypeInfo> {
-        Arc::new(TypeInfo {
-            name: format!(
-                "*{} {}",
-                if mutable { "mut" } else { "const" },
-                &pointee.name
-            ),
-            layout: Layout::new::<*const std::ffi::c_void>(),
-            data: PointerInfo { pointee, mutable }.into(),
-            immutable_pointer_type: Default::default(),
-            mutable_pointer_type: Default::default(),
-        })
-    }
-
-    /// Constructs a new struct type
-    pub fn new_struct(
-        name: impl Into<String>,
-        layout: Layout,
-        struct_info: StructInfo,
-    ) -> Arc<TypeInfo> {
-        Arc::new(TypeInfo {
-            name: name.into(),
-            layout,
-            data: TypeInfoData::Struct(struct_info),
-            immutable_pointer_type: Default::default(),
-            mutable_pointer_type: Default::default(),
-        })
-    }
-
-    /// Returns true if this instance represents the TypeInfo of the given type.
-    ///
-    /// ```rust
-    /// # use mun_memory::HasStaticTypeInfo;
-    /// assert!(i64::type_info().equals::<i64>());
-    /// assert!(!i64::type_info().equals::<f64>())
-    /// ```
-    pub fn equals<T: HasStaticTypeInfo>(&self) -> bool {
-        T::type_info().as_ref() == self
-    }
-
-    /// Returns whether this is a fundamental type.
-    pub fn is_primitive(&self) -> bool {
-        self.data.is_primitive()
-    }
-
-    /// Returns whether this is a struct type.
-    pub fn is_struct(&self) -> bool {
-        self.data.is_struct()
-    }
-
-    /// Returns whether this is a pointer type.
-    pub fn is_pointer(&self) -> bool {
-        self.data.is_pointer()
-    }
-
-    /// Returns true if this type is a concrete type. This is the case for any type that doesn't
-    /// refer to another type like a pointer.
-    pub fn is_concrete(&self) -> bool {
-        match &self.data {
-            TypeInfoData::Primitive(_) => true,
-            TypeInfoData::Struct(_) => true,
-            TypeInfoData::Pointer(_) => false,
-        }
-    }
-
-    /// Returns the GUID associated with this instance if this instance represents a concrete type.
-    pub fn as_concrete(&self) -> Option<&Guid> {
-        match &self.data {
-            TypeInfoData::Primitive(g) => Some(g),
-            TypeInfoData::Struct(s) => Some(&s.guid),
-            TypeInfoData::Pointer(_) => None,
-        }
-    }
-
-    /// Retrieves the type's struct information, if available.
-    pub fn as_struct(&self) -> Option<&StructInfo> {
-        if let TypeInfoData::Struct(s) = &self.data {
-            Some(s)
-        } else {
-            None
-        }
-    }
-
-    /// Retrieves the type's pointer information, if available.
-    pub fn as_pointer(&self) -> Option<&PointerInfo> {
-        if let TypeInfoData::Pointer(p) = &self.data {
-            Some(p)
-        } else {
-            None
-        }
-    }
-
-    /// Returns whether the type is allocated on the stack.
-    pub fn is_stack_allocated(&self) -> bool {
-        match &self.data {
-            TypeInfoData::Primitive(_) => true,
-            TypeInfoData::Struct(s) => s.memory_kind == StructMemoryKind::Value,
-            TypeInfoData::Pointer(_) => true,
-        }
-    }
-
-    /// Tries to convert from an `abi::TypeInfo`.
-    pub fn try_from_abi<'abi>(
-        type_info: &'abi abi::TypeDefinition<'abi>,
-        type_table: &TypeTable,
-    ) -> Result<TypeInfo, TryFromAbiError<'abi>> {
-        TypeInfoData::try_from_abi(&type_info.data, type_table).map(|data| TypeInfo {
-            name: type_info.name().to_owned(),
-            layout: Layout::from_size_align(type_info.size_in_bytes(), type_info.alignment())
-                .unwrap_or_else(|_| {
-                    panic!(
-                        "TypeInfo contains invalid size and alignment (size: {}, align: {})",
-                        type_info.size_in_bytes(),
-                        type_info.alignment()
-                    )
-                }),
-            data,
-            immutable_pointer_type: Default::default(),
-            mutable_pointer_type: Default::default(),
-        })
-    }
-
-    /// Returns the type that represents a pointer to this type
-    pub fn pointer_type(self: &Arc<Self>, mutable: bool) -> Arc<TypeInfo> {
-        let cache_key = if mutable {
-            &self.mutable_pointer_type
-        } else {
-            &self.immutable_pointer_type
-        };
-
-        let read_lock = cache_key.read();
-
-        // Fast path, the type already exists, return it immediately.
-        if let Some(ty) = read_lock.upgrade() {
-            return ty;
-        }
-
-        drop(read_lock);
-
-        let mut write_lock = cache_key.write();
-
-        // Recheck if another thread acquired the write lock in the mean time
-        if let Some(ty) = write_lock.upgrade() {
-            return ty;
-        }
-
-        // Otherwise create the type and store it
-        let ty = TypeInfo::new_pointer(self.clone(), mutable);
-        *write_lock = Arc::downgrade(&ty);
-
-        ty
-    }
-}
-
-impl TypeInfoData {
-    /// Returns whether this is a fundamental type.
-    pub fn is_primitive(&self) -> bool {
-        matches!(self, TypeInfoData::Primitive(_))
-    }
-
-    /// Returns whether this is a struct type.
-    pub fn is_struct(&self) -> bool {
-        matches!(self, TypeInfoData::Struct(_))
-    }
-
-    /// Returns whether this is a pointer type.
-    pub fn is_pointer(&self) -> bool {
-        matches!(self, TypeInfoData::Pointer(_))
-    }
-
-    /// Tries to convert from an `abi::TypeInfoData`.
-    pub fn try_from_abi<'abi>(
-        type_info_data: &'abi abi::TypeDefinitionData<'abi>,
-        type_table: &TypeTable,
-    ) -> Result<TypeInfoData, TryFromAbiError<'abi>> {
-        match type_info_data {
-            abi::TypeDefinitionData::Struct(s) => {
-                StructInfo::try_from_abi(s, type_table).map(TypeInfoData::Struct)
-            }
-        }
-    }
-}
-
-impl StructInfo {
-    /// Returns the `TypeInfo` and offset corresponding to the field matching the specified `field_name`, if it exists.
-    pub fn find_field_by_name<S: AsRef<str>>(&self, field_name: S) -> Option<&FieldInfo> {
-        self.fields
-            .iter()
-            .find(|field| field.name == field_name.as_ref())
-    }
-
-    /// Tries to convert from an `abi::StructInfo`.
-    pub fn try_from_abi<'abi>(
-        struct_info: &'abi abi::StructDefinition<'abi>,
-        type_table: &TypeTable,
-    ) -> Result<StructInfo, TryFromAbiError<'abi>> {
-        let fields: Result<Vec<FieldInfo>, TryFromAbiError> = izip!(
-            struct_info.field_names(),
-            struct_info.field_types(),
-            struct_info.field_offsets()
-        )
-        .map(|(name, type_id, offset)| {
-            type_table
-                .find_type_info_by_id(type_id)
-                .ok_or_else(|| TryFromAbiError::UnknownTypeId(type_id.clone()))
-                .map(|type_info| FieldInfo {
-                    name: name.to_owned(),
-                    type_info,
-                    offset: *offset,
-                })
-        })
-        .collect();
-
-        fields.map(|fields| StructInfo {
-            guid: struct_info.guid,
-            fields,
-            memory_kind: struct_info.memory_kind,
-        })
-    }
-}
-
-/// A linked version of a struct field.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct FieldInfo {
-    /// The field's name
-    pub name: String,
-    /// The field's type
-    pub type_info: Arc<TypeInfo>,
-    /// The field's offset
-    pub offset: u16,
-    // TODO: Field accessibility levels
-    // const MunPrivacy_t *field_privacies,
-}
-
-/// A trait that defines static type information for types that can provide it.
-pub trait HasStaticTypeInfo {
-    fn type_info() -> &'static Arc<TypeInfo>;
-}
-
-macro_rules! impl_primitive_type {
-    ($($ty:ty),+) => {
-        $(
-            impl HasStaticTypeInfo for $ty {
-                fn type_info() -> &'static Arc<TypeInfo> {
-                    static TYPE_INFO: once_cell::sync::OnceCell<Arc<TypeInfo>> = once_cell::sync::OnceCell::new();
-                    TYPE_INFO.get_or_init(|| {
-                         Arc::new(TypeInfo {
-                            name: <$ty as abi::PrimitiveType>::name().to_owned(),
-                            layout: Layout::new::<$ty>(),
-                            data: TypeInfoData::Primitive(<$ty as abi::PrimitiveType>::guid().clone()),
-                            immutable_pointer_type: Default::default(),
-                            mutable_pointer_type: Default::default(),
-                        })
-                    })
-                }
-            }
-        )+
-    }
-}
-
-impl_primitive_type!(
-    i8,
-    i16,
-    i32,
-    i64,
-    i128,
-    isize,
-    u8,
-    u16,
-    u32,
-    u64,
-    u128,
-    usize,
-    f32,
-    f64,
-    bool,
-    (),
-    std::ffi::c_void
-);
-
-/// Every type that has at least a type name also has a valid pointer type name
-impl<T: HasStaticTypeInfo + 'static> HasStaticTypeInfo for *mut T {
-    fn type_info() -> &'static Arc<TypeInfo> {
-        static mut VALUE: Option<StaticTypeMap<Arc<TypeInfo>>> = None;
-        static INIT: Once = Once::new();
-
-        let map = unsafe {
-            INIT.call_once(|| {
-                VALUE = Some(StaticTypeMap::default());
-            });
-            VALUE.as_ref().unwrap()
-        };
-
-        map.call_once::<T, _>(|| T::type_info().pointer_type(true))
-    }
-}
-
-/// Every type that has at least a type name also has a valid pointer type name
-impl<T: HasStaticTypeInfo + 'static> HasStaticTypeInfo for *const T {
-    fn type_info() -> &'static Arc<TypeInfo> {
-        static mut VALUE: Option<StaticTypeMap<Arc<TypeInfo>>> = None;
-        static INIT: Once = Once::new();
-
-        let map = unsafe {
-            INIT.call_once(|| {
-                VALUE = Some(StaticTypeMap::default());
-            });
-            VALUE.as_ref().unwrap()
-        };
-
-        map.call_once::<T, _>(|| T::type_info().pointer_type(false))
-    }
-}
diff --git a/crates/mun_memory/src/type_table.rs b/crates/mun_memory/src/type_table.rs
index 30b9f4e39..2772efada 100644
--- a/crates/mun_memory/src/type_table.rs
+++ b/crates/mun_memory/src/type_table.rs
@@ -1,28 +1,23 @@
-use std::sync::Arc;
-
 use rustc_hash::FxHashMap;
 
 use abi::Guid;
 
-use crate::type_info::{HasStaticTypeInfo, TypeInfo};
+use crate::r#type::{HasStaticType, Type};
 
 #[derive(Clone)]
 pub struct TypeTable {
-    concrete: FxHashMap<Guid, Arc<TypeInfo>>,
-    type_name_to_type_info: FxHashMap<String, Arc<TypeInfo>>,
+    concrete: FxHashMap<Guid, Type>,
+    type_name_to_type_info: FxHashMap<String, Type>,
 }
 
 impl TypeTable {
     /// Returns the TypeInfo for the type with the given name.
-    pub fn find_type_info_by_name<S: AsRef<str>>(&self, name: S) -> Option<Arc<TypeInfo>> {
+    pub fn find_type_info_by_name<S: AsRef<str>>(&self, name: S) -> Option<Type> {
         self.type_name_to_type_info.get(name.as_ref()).cloned()
     }
 
     /// Returns the [`TypeInfo`] referenced by the given [`abi::TypeId`].
-    pub fn find_type_info_by_id<'abi>(
-        &self,
-        type_id: &'abi abi::TypeId<'abi>,
-    ) -> Option<Arc<TypeInfo>> {
+    pub fn find_type_info_by_id<'abi>(&self, type_id: &'abi abi::TypeId<'abi>) -> Option<Type> {
         match type_id {
             abi::TypeId::Concrete(guid) => self.concrete.get(guid).cloned(),
             abi::TypeId::Pointer(p) => self
@@ -35,7 +30,7 @@ impl TypeTable {
     ///
     /// If the type table already contained this `type_info`, the value is updated, and the old
     /// value is returned.
-    fn insert_static_type<T: HasStaticTypeInfo>(&mut self) -> Option<Arc<TypeInfo>> {
+    fn insert_static_type<T: HasStaticType>(&mut self) -> Option<Type> {
         self.insert_type(T::type_info().clone())
     }
 
@@ -43,23 +38,29 @@ impl TypeTable {
     ///
     /// If the type table already contained this `type_info`, the value is updated, and the old
     /// value is returned.
-    pub fn insert_type(&mut self, type_info: Arc<TypeInfo>) -> Option<Arc<TypeInfo>> {
+    pub fn insert_type(&mut self, type_info: Type) -> Option<Type> {
         match type_info.as_concrete() {
             None => panic!("can only insert concrete types"),
-            Some(guid) => {
-                self.type_name_to_type_info
-                    .insert(type_info.name.clone(), type_info.clone());
-                self.concrete.insert(*guid, type_info)
-            }
+            Some(guid) => self.insert_concrete_type(*guid, type_info),
         }
     }
 
+    /// Inserts the concrete `Type` into the type table.
+    ///
+    /// If the type table already contained this `type_info`, the value is updated, and the old
+    /// value is returned.
+    pub fn insert_concrete_type(&mut self, guid: Guid, ty: Type) -> Option<Type> {
+        self.type_name_to_type_info
+            .insert(ty.name().to_owned(), ty.clone());
+        self.concrete.insert(guid, ty)
+    }
+
     /// Removes the specified TypeInfo from the lookup table.
-    pub fn remove_type(&mut self, ty: &TypeInfo) -> Option<Arc<TypeInfo>> {
+    pub fn remove_type(&mut self, ty: &Type) -> Option<Type> {
         match ty.as_concrete() {
             None => panic!("can only remove concrete types"),
             Some(guid) => {
-                self.type_name_to_type_info.remove(&ty.name);
+                self.type_name_to_type_info.remove(ty.name());
                 self.concrete.remove(guid)
             }
         }
@@ -70,18 +71,13 @@ impl TypeTable {
     pub fn remove_type_by_type_info<'abi>(
         &mut self,
         type_info: &'abi abi::TypeDefinition<'abi>,
-    ) -> Option<Arc<TypeInfo>> {
-        match type_info.as_concrete() {
-            None => panic!("can only remove concrete types"),
-            Some(guid) => {
-                let ty = self.concrete.remove(guid)?;
-                self.type_name_to_type_info.remove(&ty.name)
-            }
-        }
+    ) -> Option<Type> {
+        let ty = self.concrete.remove(type_info.as_concrete())?;
+        self.type_name_to_type_info.remove(ty.name())
     }
 
     /// Removes and returns the `TypeInfo` corresponding to `name`, if it exists.
-    pub fn remove_type_by_name<S: AsRef<str>>(&mut self, name: S) -> Option<Arc<TypeInfo>> {
+    pub fn remove_type_by_name<S: AsRef<str>>(&mut self, name: S) -> Option<Type> {
         if let Some(type_info) = self.type_name_to_type_info.remove(name.as_ref()) {
             self.remove_type(&type_info)
         } else {
diff --git a/crates/mun_runtime/Cargo.toml b/crates/mun_runtime/Cargo.toml
index c51635706..782890e0e 100644
--- a/crates/mun_runtime/Cargo.toml
+++ b/crates/mun_runtime/Cargo.toml
@@ -20,6 +20,7 @@ libloader = { version = "=0.1.0", path = "../mun_libloader", package = "mun_libl
 log = "0.4"
 md5 = "0.7.0"
 memory = { version = "=0.2.0", path = "../mun_memory", package = "mun_memory" }
+capi_utils = { version = "=0.1.0", path = "../mun_capi_utils", package = "mun_capi_utils" }
 mun_project = { version = "=0.1.0", path = "../mun_project" }
 notify = "4.0.12"
 once_cell = "1.4.0"
diff --git a/crates/mun_runtime/src/adt.rs b/crates/mun_runtime/src/adt.rs
index 71eac7a61..82c7cd130 100644
--- a/crates/mun_runtime/src/adt.rs
+++ b/crates/mun_runtime/src/adt.rs
@@ -6,7 +6,7 @@ use crate::{
 };
 use memory::{
     gc::{GcPtr, GcRuntime, HasIndirectionPtr},
-    TypeInfo,
+    Type,
 };
 use std::{
     ptr::{self, NonNull},
@@ -53,7 +53,7 @@ impl<'s> StructRef<'s> {
     }
 
     /// Returns the type information of the struct.
-    pub fn type_info(&self) -> Arc<TypeInfo> {
+    pub fn type_info(&self) -> Type {
         self.runtime.gc.ptr_type(self.raw.0)
     }
 
@@ -79,30 +79,33 @@ impl<'s> StructRef<'s> {
         // Safety: `as_struct` is guaranteed to return `Some` for `StructRef`s.
         let struct_info = type_info.as_struct().unwrap();
 
-        let field_info = struct_info.find_field_by_name(field_name).ok_or_else(|| {
-            format!(
-                "Struct `{}` does not contain field `{}`.",
-                type_info.name, field_name
-            )
-        })?;
-
-        if !T::accepts_type(&field_info.type_info) {
+        let field_info = struct_info
+            .fields()
+            .find_by_name(field_name)
+            .ok_or_else(|| {
+                format!(
+                    "Struct `{}` does not contain field `{}`.",
+                    type_info.name(),
+                    field_name
+                )
+            })?;
+
+        if !T::accepts_type(&field_info.ty()) {
             return Err(format!(
                 "Mismatched types for `{}::{}`. Expected: `{}`. Found: `{}`.",
-                type_info.name,
+                type_info.name(),
                 field_name,
                 T::type_hint(),
-                field_info.type_info.name,
+                field_info.ty().name(),
             ));
         };
 
         // SAFETY: The offset in the ABI is always valid.
-        let field_ptr =
-            unsafe { self.get_field_ptr_unchecked::<T::MunType>(usize::from(field_info.offset)) };
+        let field_ptr = unsafe { self.get_field_ptr_unchecked::<T::MunType>(field_info.offset()) };
         Ok(Marshal::marshal_from_ptr(
             field_ptr,
             self.runtime,
-            &field_info.type_info,
+            &field_info.ty(),
         ))
     }
 
@@ -121,26 +124,32 @@ impl<'s> StructRef<'s> {
         // Safety: `as_struct` is guaranteed to return `Some` for `StructRef`s.
         let struct_info = type_info.as_struct().unwrap();
 
-        let field_info = struct_info.find_field_by_name(field_name).ok_or_else(|| {
-            format!(
-                "Struct `{}` does not contain field `{}`.",
-                type_info.name, field_name
-            )
-        })?;
+        let field_info = struct_info
+            .fields()
+            .find_by_name(field_name)
+            .ok_or_else(|| {
+                format!(
+                    "Struct `{}` does not contain field `{}`.",
+                    type_info.name(),
+                    field_name
+                )
+            })?;
 
         let value_type = value.type_info(self.runtime);
-        if field_info.type_info != value_type {
+        if field_info.ty() != value_type {
             return Err(format!(
                 "Mismatched types for `{}::{}`. Expected: `{}`. Found: `{}`.",
-                type_info.name, field_name, value_type.name, field_info.type_info
+                type_info.name(),
+                field_name,
+                value_type.name(),
+                field_info.ty()
             ));
         }
 
         // SAFETY: The offset in the ABI is always valid.
-        let field_ptr =
-            unsafe { self.get_field_ptr_unchecked::<T::MunType>(usize::from(field_info.offset)) };
-        let old = Marshal::marshal_from_ptr(field_ptr, self.runtime, &field_info.type_info);
-        Marshal::marshal_to_ptr(value, field_ptr, &field_info.type_info);
+        let field_ptr = unsafe { self.get_field_ptr_unchecked::<T::MunType>(field_info.offset()) };
+        let old = Marshal::marshal_from_ptr(field_ptr, self.runtime, &field_info.ty());
+        Marshal::marshal_to_ptr(value, field_ptr, &field_info.ty());
         Ok(old)
     }
 
@@ -155,31 +164,37 @@ impl<'s> StructRef<'s> {
         // Safety: `as_struct` is guaranteed to return `Some` for `StructRef`s.
         let struct_info = type_info.as_struct().unwrap();
 
-        let field_info = struct_info.find_field_by_name(field_name).ok_or_else(|| {
-            format!(
-                "Struct `{}` does not contain field `{}`.",
-                type_info.name, field_name
-            )
-        })?;
+        let field_info = struct_info
+            .fields()
+            .find_by_name(field_name)
+            .ok_or_else(|| {
+                format!(
+                    "Struct `{}` does not contain field `{}`.",
+                    type_info.name(),
+                    field_name
+                )
+            })?;
 
         let value_type = value.type_info(self.runtime);
-        if field_info.type_info != value_type {
+        if field_info.ty() != value_type {
             return Err(format!(
                 "Mismatched types for `{}::{}`. Expected: `{}`. Found: `{}`.",
-                type_info.name, field_name, value_type.name, field_info.type_info
+                type_info.name(),
+                field_name,
+                value_type.name(),
+                field_info.ty()
             ));
         }
 
         // SAFETY: The offset in the ABI is always valid.
-        let field_ptr =
-            unsafe { self.get_field_ptr_unchecked::<T::MunType>(usize::from(field_info.offset)) };
-        Marshal::marshal_to_ptr(value, field_ptr, &field_info.type_info);
+        let field_ptr = unsafe { self.get_field_ptr_unchecked::<T::MunType>(field_info.offset()) };
+        Marshal::marshal_to_ptr(value, field_ptr, &field_info.ty());
         Ok(())
     }
 }
 
 impl<'r> ArgumentReflection for StructRef<'r> {
-    fn type_info(&self, _runtime: &Runtime) -> Arc<TypeInfo> {
+    fn type_info(&self, _runtime: &Runtime) -> Type {
         self.type_info()
     }
 }
@@ -201,7 +216,7 @@ impl<'s> Marshal<'s> for StructRef<'s> {
     fn marshal_from_ptr<'r>(
         ptr: NonNull<Self::MunType>,
         runtime: &'r Runtime,
-        type_info: &Arc<TypeInfo>,
+        type_info: &Type,
     ) -> StructRef<'s>
     where
         Self: 's,
@@ -210,7 +225,7 @@ impl<'s> Marshal<'s> for StructRef<'s> {
         let struct_info = type_info.as_struct().unwrap();
 
         // Copy the contents of the struct based on what kind of pointer we are dealing with
-        let gc_handle = if struct_info.memory_kind == abi::StructMemoryKind::Value {
+        let gc_handle = if struct_info.is_value_struct() {
             // For a value struct, `ptr` points to a struct value.
 
             // Create a new object using the runtime's intrinsic
@@ -219,7 +234,7 @@ impl<'s> Marshal<'s> for StructRef<'s> {
             // Construct
             let src = ptr.cast::<u8>().as_ptr() as *const _;
             let dest = unsafe { gc_handle.deref_mut::<u8>() };
-            unsafe { ptr::copy_nonoverlapping(src, dest, type_info.layout.size()) };
+            unsafe { ptr::copy_nonoverlapping(src, dest, type_info.layout().size()) };
 
             gc_handle
         } else {
@@ -230,12 +245,16 @@ impl<'s> Marshal<'s> for StructRef<'s> {
         StructRef::new(RawStruct(gc_handle), runtime)
     }
 
-    fn marshal_to_ptr(value: Self, mut ptr: NonNull<Self::MunType>, type_info: &Arc<TypeInfo>) {
+    fn marshal_to_ptr(value: Self, mut ptr: NonNull<Self::MunType>, type_info: &Type) {
         let struct_info = type_info.as_struct().unwrap();
-        if struct_info.memory_kind == abi::StructMemoryKind::Value {
+        if struct_info.is_value_struct() {
             let dest = ptr.cast::<u8>().as_ptr();
             unsafe {
-                ptr::copy_nonoverlapping(value.into_raw().get_ptr(), dest, type_info.layout.size())
+                ptr::copy_nonoverlapping(
+                    value.into_raw().get_ptr(),
+                    dest,
+                    type_info.layout().size(),
+                )
             };
         } else {
             unsafe { *ptr.as_mut() = value.into_raw() };
@@ -245,7 +264,7 @@ impl<'s> Marshal<'s> for StructRef<'s> {
 
 impl<'r> ReturnTypeReflection for StructRef<'r> {
     /// Returns true if this specified type can be stored in an instance of this type
-    fn accepts_type(ty: &Arc<TypeInfo>) -> bool {
+    fn accepts_type(ty: &Type) -> bool {
         ty.is_struct()
     }
 
diff --git a/crates/mun_runtime/src/assembly.rs b/crates/mun_runtime/src/assembly.rs
index 588e1214b..11eb94035 100644
--- a/crates/mun_runtime/src/assembly.rs
+++ b/crates/mun_runtime/src/assembly.rs
@@ -13,7 +13,7 @@ use libloader::{MunLibrary, TempLibrary};
 use memory::{
     mapping::{Mapping, MemoryMapper},
     type_table::TypeTable,
-    TryFromAbiError, TypeInfo,
+    Type,
 };
 
 use crate::{garbage_collector::GarbageCollector, DispatchTable};
@@ -71,15 +71,15 @@ impl Assembly {
     }
 
     fn link_all_types<'abi>(
-        type_table: &mut TypeTable,
+        type_table: &TypeTable,
         to_link: impl Iterator<Item = (&'abi abi::TypeId<'abi>, &'abi mut *const c_void, &'abi str)>,
     ) -> anyhow::Result<()> {
         // Try to link all LUT entries
         let mut failed_to_link = false;
         for (type_id, type_info_ptr, debug_name) in to_link {
             // Ensure that the function is in the runtime dispatch table
-            if let Some(type_info) = type_table.find_type_info_by_id(type_id) {
-                *type_info_ptr = Arc::into_raw(type_info) as *const c_void;
+            if let Some(ty) = type_table.find_type_info_by_id(type_id) {
+                *type_info_ptr = Type::into_raw(ty);
             } else {
                 dbg!(debug_name);
                 failed_to_link = true;
@@ -152,20 +152,20 @@ impl Assembly {
                     {
                         let expected = fn_proto_arg_type_infos
                             .iter()
-                            .map(|ty| ty.name.clone())
+                            .map(|ty| ty.name().to_owned())
                             .join(", ");
                         let found = existing_fn_def
                             .prototype
                             .signature
                             .arg_types
                             .iter()
-                            .map(|ty| ty.name.clone())
+                            .map(|ty| ty.name().to_owned())
                             .join(", ");
 
                         let fn_name = fn_prototype.name();
                         return Err(anyhow!("a function with the same name does exist, but the signatures do not match.\nExpected:\n\tfn {fn_name}({expected}) -> {}\n\nFound:\n\tfn {fn_name}({found}) -> {}",
-                            &fn_proto_ret_type_info.name,
-                            &existing_fn_def.prototype.signature.return_type.name))
+                            fn_proto_ret_type_info.name(),
+                            existing_fn_def.prototype.signature.return_type.name()))
                             .with_context(|| format!("failed to link function '{}'", fn_prototype.name()));
                     }
 
@@ -203,24 +203,14 @@ impl Assembly {
     ) -> anyhow::Result<(DispatchTable, TypeTable)> {
         let mut assemblies: Vec<&'a mut _> = assemblies.collect();
 
-        // Clone the type table, such that we can roll back if linking fails
-        let mut type_table = type_table.clone();
-
-        // Collect all types that need to be loaded
-        let mut types_to_load: VecDeque<&abi::TypeDefinition> = assemblies
-            .iter()
-            .flat_map(|asm| asm.info().symbols.types().iter())
-            .collect();
-
-        // Load all types
-        while let Some(type_info) = types_to_load.pop_front() {
-            match TypeInfo::try_from_abi(type_info, &type_table) {
-                Ok(type_info) => {
-                    assert!(type_table.insert_type(Arc::new(type_info)).is_none())
-                }
-                Err(TryFromAbiError::UnknownTypeId(_)) => types_to_load.push_back(type_info),
-            }
-        }
+        // Load all types, this creates a new type table that contains the types loaded
+        let (type_table, _) = Type::try_from_abi(
+            assemblies
+                .iter()
+                .flat_map(|asm| asm.info().symbols.types().iter()),
+            type_table.clone(),
+        )
+        .map_err(|e| anyhow::anyhow!("failed to link types from assembly: {}", e))?;
 
         let types_to_link = assemblies
             .iter_mut()
@@ -229,7 +219,7 @@ impl Assembly {
             // by the compiler.
             .filter(|(_, ptr, _)| ptr.is_null());
 
-        Assembly::link_all_types(&mut type_table, types_to_link)?;
+        Assembly::link_all_types(&type_table, types_to_link)?;
 
         // Clone the dispatch table, such that we can roll back if linking fails
         let mut dispatch_table = dispatch_table.clone();
@@ -248,6 +238,9 @@ impl Assembly {
 
         Assembly::link_all_functions(&dispatch_table, &type_table, functions_to_link)?;
 
+        // Collect remaining types
+        Type::collect_unreferenced_type_data();
+
         Ok((dispatch_table, type_table))
     }
 
@@ -294,44 +287,30 @@ impl Assembly {
                 continue;
             }
 
-            let old_types: Option<(&Assembly, Vec<Arc<TypeInfo>>)> =
-                old_assembly.map(|old_assembly| {
-                    // Remove the old assemblies' types from the type table
-                    let old_types = old_assembly
-                        .info()
-                        .symbols
-                        .types()
-                        .iter()
-                        .map(|type_info| {
-                            type_table.remove_type_by_type_info(type_info).expect(
-                                "All types from a loaded assembly must exist in the type table.",
-                            )
-                        })
-                        .collect();
-
-                    (old_assembly, old_types)
-                });
+            let old_types: Option<(&Assembly, Vec<Type>)> = old_assembly.map(|old_assembly| {
+                // Remove the old assemblies' types from the type table
+                let old_types = old_assembly
+                    .info()
+                    .symbols
+                    .types()
+                    .iter()
+                    .map(|type_info| {
+                        type_table.remove_type_by_type_info(type_info).expect(
+                            "All types from a loaded assembly must exist in the type table.",
+                        )
+                    })
+                    .collect();
 
-            // Collect all types that need to be loaded
-            let mut types_to_load: VecDeque<&abi::TypeDefinition> =
-                new_assembly.info.symbols.types().iter().collect();
+                (old_assembly, old_types)
+            });
 
-            let mut new_types = Vec::with_capacity(types_to_load.len());
+            // Collect all types that need to be loaded
+            let (updated_type_table, new_types) =
+                Type::try_from_abi(new_assembly.info.symbols.types(), type_table)
+                    .map_err(|e| anyhow::anyhow!("failed to load assembly types: {}", e))?;
+            type_table = updated_type_table;
 
             // Load all types, retrying types that depend on other unloaded types within the module
-            while let Some(type_info) = types_to_load.pop_front() {
-                match TypeInfo::try_from_abi(type_info, &type_table) {
-                    Ok(type_info) => {
-                        let type_info = Arc::new(type_info);
-                        new_types.push(type_info.clone());
-                        assert!(type_table.insert_type(type_info).is_none());
-                    }
-                    Err(TryFromAbiError::UnknownTypeId(_)) => {
-                        types_to_load.push_back(type_info);
-                    }
-                }
-            }
-
             let types_to_link = new_assembly
                 .info_mut()
                 .type_lut
@@ -340,7 +319,7 @@ impl Assembly {
                 // by the compiler.
                 .filter(|(_, ptr, _)| ptr.is_null());
 
-            Assembly::link_all_types(&mut type_table, types_to_link)?;
+            Assembly::link_all_types(&type_table, types_to_link)?;
 
             // Memory map allocated object
             if let Some((old_assembly, old_types)) = old_types {
@@ -396,6 +375,9 @@ impl Assembly {
             linked_assemblies.insert(new_path, new_assembly);
         }
 
+        // Collect types
+        Type::collect_unreferenced_type_data();
+
         Ok((dispatch_table, type_table))
     }
 
diff --git a/crates/mun_runtime/src/function_info.rs b/crates/mun_runtime/src/function_info.rs
index 3010f6050..86be7b7cb 100644
--- a/crates/mun_runtime/src/function_info.rs
+++ b/crates/mun_runtime/src/function_info.rs
@@ -1,6 +1,6 @@
-use std::{ffi::c_void, sync::Arc};
+use std::{ffi::c_void, ptr, sync::Arc};
 
-use memory::{type_table::TypeTable, TryFromAbiError, TypeInfo};
+use memory::{type_table::TypeTable, HasStaticType, TryFromAbiError, Type};
 
 /// A linked version of [`mun_abi::FunctionDefinition`] that has resolved all occurrences of `TypeId` with `TypeInfo`.
 #[derive(Clone)]
@@ -11,6 +11,18 @@ pub struct FunctionDefinition {
     pub fn_ptr: *const c_void,
 }
 
+impl FunctionDefinition {
+    /// Creates a builder to easily create a new `FunctionDefinition`.
+    pub fn builder(name: impl Into<String>) -> FunctionDefinitionBuilder {
+        FunctionDefinitionBuilder {
+            name: name.into(),
+            arg_types: vec![],
+            return_type: <()>::type_info().clone(),
+            fn_ptr: ptr::null(),
+        }
+    }
+}
+
 unsafe impl Send for FunctionDefinition {}
 unsafe impl Sync for FunctionDefinition {}
 
@@ -57,9 +69,9 @@ impl FunctionPrototype {
 #[derive(Clone)]
 pub struct FunctionSignature {
     /// Argument types
-    pub arg_types: Vec<Arc<TypeInfo>>,
+    pub arg_types: Vec<Type>,
     /// Return type
-    pub return_type: Arc<TypeInfo>,
+    pub return_type: Type,
 }
 
 impl FunctionSignature {
@@ -68,7 +80,7 @@ impl FunctionSignature {
         fn_sig: &'abi abi::FunctionSignature<'abi>,
         type_table: &TypeTable,
     ) -> Result<Self, TryFromAbiError<'abi>> {
-        let arg_types: Vec<Arc<TypeInfo>> = fn_sig
+        let arg_types: Vec<Type> = fn_sig
             .arg_types()
             .iter()
             .map(|type_id| {
@@ -100,7 +112,7 @@ macro_rules! into_function_info_impl {
         extern "C" fn($($T:ident),*) -> $R:ident;
     )+) => {
         $(
-            impl<$R: memory::HasStaticTypeInfo, $($T: memory::HasStaticTypeInfo,)*> IntoFunctionDefinition
+            impl<$R: memory::HasStaticType, $($T: memory::HasStaticType,)*> IntoFunctionDefinition
             for extern "C" fn($($T),*) -> $R
             {
                 fn into<S: Into<String>>(self, name: S) -> FunctionDefinition {
@@ -109,8 +121,8 @@ macro_rules! into_function_info_impl {
                         prototype: FunctionPrototype {
                             name: name.into(),
                             signature: FunctionSignature {
-                                arg_types: vec![$(<$T as memory::HasStaticTypeInfo>::type_info().clone(),)*],
-                                return_type: <R as memory::HasStaticTypeInfo>::type_info().clone(),
+                                arg_types: vec![$(<$T as memory::HasStaticType>::type_info().clone(),)*],
+                                return_type: <R as memory::HasStaticType>::type_info().clone(),
                             }
                         }
                     }
@@ -133,3 +145,54 @@ into_function_info_impl! {
     extern "C" fn(A, B, C, D, E, F, G, H, I) -> R;
     extern "C" fn(A, B, C, D, E, F, G, H, I, J) -> R;
 }
+
+/// A helper struct to ergonomically build functions.
+#[derive(Debug)]
+pub struct FunctionDefinitionBuilder {
+    name: String,
+    arg_types: Vec<Type>,
+    return_type: Type,
+    fn_ptr: *const c_void,
+}
+
+impl FunctionDefinitionBuilder {
+    /// Adds an argument
+    pub fn add_argument(mut self, arg: Type) -> Self {
+        self.arg_types.push(arg);
+        self
+    }
+
+    /// Adds arguments
+    pub fn add_arguments(mut self, iter: impl IntoIterator<Item = Type>) -> Self {
+        for arg in iter.into_iter() {
+            self.arg_types.push(arg);
+        }
+        self
+    }
+
+    /// Sets the return type
+    pub fn set_return_type(mut self, ty: Type) -> Self {
+        self.return_type = ty;
+        self
+    }
+
+    /// Sets the function pointer
+    pub fn set_ptr(mut self, ptr: *const c_void) -> Self {
+        self.fn_ptr = ptr;
+        self
+    }
+
+    /// Construct the [`FunctionDefinition`]
+    pub fn finish(self) -> Arc<FunctionDefinition> {
+        Arc::new(FunctionDefinition {
+            prototype: FunctionPrototype {
+                name: self.name,
+                signature: FunctionSignature {
+                    arg_types: self.arg_types,
+                    return_type: self.return_type,
+                },
+            },
+            fn_ptr: self.fn_ptr,
+        })
+    }
+}
diff --git a/crates/mun_runtime/src/lib.rs b/crates/mun_runtime/src/lib.rs
index a2204f626..72e950c52 100644
--- a/crates/mun_runtime/src/lib.rs
+++ b/crates/mun_runtime/src/lib.rs
@@ -46,7 +46,7 @@ pub use crate::{
     reflection::{ArgumentReflection, ReturnTypeReflection},
 };
 // Re-export some useful types so crates dont have to depend on mun_memory as well.
-pub use memory::{FieldInfo, HasStaticTypeInfo, StructInfo, TypeFields, TypeInfo};
+pub use memory::{Field, FieldData, HasStaticType, PointerType, StructType, Type};
 
 /// Options for the construction of a [`Runtime`].
 pub struct RuntimeOptions {
@@ -73,8 +73,8 @@ unsafe fn get_allocator(alloc_handle: *mut ffi::c_void) -> Arc<GarbageCollector>
 /// # Safety
 ///
 /// The type handle must have been returned from a call to [`Arc<TypeInfo>::into_raw`][into_raw].
-unsafe fn get_type_info(type_handle: *const ffi::c_void) -> Arc<TypeInfo> {
-    Arc::from_raw(type_handle as *const TypeInfo)
+unsafe fn get_type_info(type_handle: *const ffi::c_void) -> Type {
+    Type::from_raw(type_handle)
 }
 
 extern "C" fn new(
@@ -298,12 +298,12 @@ impl Runtime {
     }
 
     /// Retrieves the type definition corresponding to `type_name`, if available.
-    pub fn get_type_info_by_name(&self, type_name: &str) -> Option<Arc<TypeInfo>> {
+    pub fn get_type_info_by_name(&self, type_name: &str) -> Option<Type> {
         self.type_table.find_type_info_by_name(type_name)
     }
 
     /// Retrieve the type information corresponding to the `type_id`, if available.
-    pub fn get_type_info_by_id(&self, type_id: &abi::TypeId) -> Option<Arc<TypeInfo>> {
+    pub fn get_type_info_by_id(&self, type_id: &abi::TypeId) -> Option<Type> {
         self.type_table.find_type_info_by_id(type_id)
     }
 
@@ -564,8 +564,8 @@ seq_macro::seq!(I in 0..N {
                 return Err(format!(
                     "Invalid argument type at index {}. Expected: {}. Found: {}.",
                     I,
-                    self.I.type_info(runtime).name,
-                    arg_types[I].name,
+                    self.I.type_info(runtime).name(),
+                    arg_types[I].name(),
                 ));
             }
             )*
@@ -632,7 +632,7 @@ impl Runtime {
             return Err(InvokeErr {
                 msg: format!(
                     "unexpected return type, got '{}', expected '{}",
-                    &function_info.prototype.signature.return_type.name,
+                    &function_info.prototype.signature.return_type.name(),
                     ReturnType::type_hint()
                 ),
                 function_name,
diff --git a/crates/mun_runtime/src/marshal.rs b/crates/mun_runtime/src/marshal.rs
index 8221e62ab..d9f4fd081 100644
--- a/crates/mun_runtime/src/marshal.rs
+++ b/crates/mun_runtime/src/marshal.rs
@@ -1,7 +1,7 @@
-use memory::TypeInfo;
+use memory::Type;
 
 use crate::Runtime;
-use std::{ptr::NonNull, sync::Arc};
+use std::ptr::NonNull;
 
 /// Used to do value-to-value conversions that require runtime type information while consuming the
 /// input value.
@@ -24,12 +24,12 @@ pub trait Marshal<'t>: Sized {
     fn marshal_from_ptr<'r>(
         ptr: NonNull<Self::MunType>,
         runtime: &'r Runtime,
-        type_info: &Arc<TypeInfo>,
+        type_info: &Type,
     ) -> Self
     where
         Self: 't,
         'r: 't;
 
     /// Marshals `value` to memory location `ptr` (i.e. Rust -> Mun).
-    fn marshal_to_ptr(value: Self, ptr: NonNull<Self::MunType>, type_info: &Arc<TypeInfo>);
+    fn marshal_to_ptr(value: Self, ptr: NonNull<Self::MunType>, type_info: &Type);
 }
diff --git a/crates/mun_runtime/src/reflection.rs b/crates/mun_runtime/src/reflection.rs
index 1357ef880..e1be57c80 100644
--- a/crates/mun_runtime/src/reflection.rs
+++ b/crates/mun_runtime/src/reflection.rs
@@ -1,11 +1,10 @@
 use crate::{marshal::Marshal, Runtime};
-use memory::{HasStaticTypeInfo, TypeInfo};
-use std::sync::Arc;
+use memory::{HasStaticType, Type};
 
 /// A type to emulate dynamic typing across compilation units for static types.
 pub trait ReturnTypeReflection: Sized {
     /// Returns true if this specified type can be stored in an instance `Self`.
-    fn accepts_type(ty: &Arc<TypeInfo>) -> bool;
+    fn accepts_type(ty: &Type) -> bool;
 
     /// Returns a type hint to indicate the name of this type
     fn type_hint() -> &'static str;
@@ -14,25 +13,25 @@ pub trait ReturnTypeReflection: Sized {
 /// A type to emulate dynamic typing across compilation units for statically typed values.
 pub trait ArgumentReflection: Sized {
     /// Retrieves the argument's type information.
-    fn type_info(&self, runtime: &Runtime) -> Arc<TypeInfo>;
+    fn type_info(&self, runtime: &Runtime) -> Type;
 }
 
 macro_rules! impl_primitive_type {
     ($($ty:ty),+) => {
         $(
             impl ArgumentReflection for $ty {
-                fn type_info(&self, _runtime: &Runtime) -> Arc<TypeInfo> {
-                    <Self as HasStaticTypeInfo>::type_info().clone()
+                fn type_info(&self, _runtime: &Runtime) -> Type {
+                    <Self as HasStaticType>::type_info().clone()
                 }
             }
 
             impl ReturnTypeReflection for $ty {
-                fn accepts_type(ty: &Arc<TypeInfo>) -> bool {
-                    ty == <Self as HasStaticTypeInfo>::type_info()
+                fn accepts_type(ty: &Type) -> bool {
+                    ty == <Self as HasStaticType>::type_info()
                 }
 
                 fn type_hint() -> &'static str {
-                    &<Self as HasStaticTypeInfo>::type_info().name
+                    <Self as HasStaticType>::type_info().name()
                 }
             }
 
@@ -54,7 +53,7 @@ macro_rules! impl_primitive_type {
                 fn marshal_from_ptr<'r>(
                     ptr: std::ptr::NonNull<Self::MunType>,
                     _runtime: &'r Runtime,
-                    _type_info: &Arc<TypeInfo>,
+                    _type_info: &Type,
                 ) -> Self
                 where
                     Self: 't,
@@ -68,7 +67,7 @@ macro_rules! impl_primitive_type {
                 fn marshal_to_ptr(
                     value: Self,
                     mut ptr: std::ptr::NonNull<Self::MunType>,
-                    _type_info: &Arc<TypeInfo>,
+                    _type_info: &Type,
                 ) {
                     unsafe { *ptr.as_mut() = value };
                 }
@@ -98,44 +97,44 @@ impl_primitive_type!(
 
 impl<T> ArgumentReflection for *const T
 where
-    *const T: HasStaticTypeInfo,
+    *const T: HasStaticType,
 {
-    fn type_info(&self, _runtime: &Runtime) -> Arc<TypeInfo> {
-        <Self as HasStaticTypeInfo>::type_info().clone()
+    fn type_info(&self, _runtime: &Runtime) -> Type {
+        <Self as HasStaticType>::type_info().clone()
     }
 }
 
 impl<T> ReturnTypeReflection for *const T
 where
-    *const T: HasStaticTypeInfo,
+    *const T: HasStaticType,
 {
-    fn accepts_type(ty: &Arc<TypeInfo>) -> bool {
-        <*const T as HasStaticTypeInfo>::type_info() == ty
+    fn accepts_type(ty: &Type) -> bool {
+        <*const T as HasStaticType>::type_info() == ty
     }
 
     fn type_hint() -> &'static str {
-        &<*const T as HasStaticTypeInfo>::type_info().name
+        <*const T as HasStaticType>::type_info().name()
     }
 }
 
 impl<T> ArgumentReflection for *mut T
 where
-    *mut T: HasStaticTypeInfo,
+    *mut T: HasStaticType,
 {
-    fn type_info(&self, _runtime: &Runtime) -> Arc<TypeInfo> {
-        <Self as HasStaticTypeInfo>::type_info().clone()
+    fn type_info(&self, _runtime: &Runtime) -> Type {
+        <Self as HasStaticType>::type_info().clone()
     }
 }
 
 impl<T> ReturnTypeReflection for *mut T
 where
-    *mut T: HasStaticTypeInfo,
+    *mut T: HasStaticType,
 {
-    fn accepts_type(ty: &Arc<TypeInfo>) -> bool {
-        <*mut T as HasStaticTypeInfo>::type_info() == ty
+    fn accepts_type(ty: &Type) -> bool {
+        <*mut T as HasStaticType>::type_info() == ty
     }
 
     fn type_hint() -> &'static str {
-        &<*mut T as HasStaticTypeInfo>::type_info().name
+        <*mut T as HasStaticType>::type_info().name()
     }
 }
diff --git a/crates/mun_runtime_capi/Cargo.toml b/crates/mun_runtime_capi/Cargo.toml
index 622dadc7f..b2f4b3dd0 100644
--- a/crates/mun_runtime_capi/Cargo.toml
+++ b/crates/mun_runtime_capi/Cargo.toml
@@ -20,6 +20,8 @@ crate-type = ["cdylib"]
 abi = { version = "=0.3.0", path = "../mun_abi", package = "mun_abi" }
 memory = { version = "=0.2.0", path = "../mun_memory", package = "mun_memory" }
 runtime = { version = "=0.3.0", path = "../mun_runtime", package = "mun_runtime" }
+capi_utils = { version = "=0.1.0", path = "../mun_capi_utils", package = "mun_capi_utils", features=["insta"]}
+insta = { version = "1.12.0", features=["ron"] }
 
 [dev-dependencies]
 compiler = { path="../mun_compiler", package = "mun_compiler" }
diff --git a/crates/mun_runtime_capi/cbindgen.toml b/crates/mun_runtime_capi/cbindgen.toml
index f004f637b..6ffcbb486 100644
--- a/crates/mun_runtime_capi/cbindgen.toml
+++ b/crates/mun_runtime_capi/cbindgen.toml
@@ -10,9 +10,12 @@ line_length = 100
 tab_width = 4
 
 [export]
-include = ["StructInfo"]
 prefix = "Mun"
 
+[enum]
+rename_variants = "QualifiedScreamingSnakeCase"
+
 [parse]
 parse_deps = true
-include = ["mun_abi", "mun_memory", "mun_runtime"]
+include = ["mun_abi", "mun_memory", "mun_runtime", "mun_capi_utils"]
+extra_bindings = ["mun_memory", "mun_capi_utils"]
diff --git a/crates/mun_runtime_capi/src/error.rs b/crates/mun_runtime_capi/src/error.rs
deleted file mode 100644
index 21325ab07..000000000
--- a/crates/mun_runtime_capi/src/error.rs
+++ /dev/null
@@ -1,36 +0,0 @@
-//! Exposes error reporting using the C ABI.
-
-use std::{ffi::CString, os::raw::c_char, ptr};
-
-#[repr(C)]
-#[derive(Clone, Copy)]
-/// A C-style handle to an error message.
-///
-/// If the handle contains a non-null pointer, an error occurred.
-pub struct ErrorHandle(pub *const c_char);
-
-impl ErrorHandle {
-    /// Constructs an `ErrorHandle` from the specified error message.
-    pub(crate) fn new<T: Into<Vec<u8>>>(error_message: T) -> Self {
-        let error_message = CString::new(error_message).expect("Invalid error message");
-        Self(CString::into_raw(error_message))
-    }
-}
-
-impl Default for ErrorHandle {
-    fn default() -> Self {
-        Self(ptr::null())
-    }
-}
-
-/// Destructs the error message corresponding to the specified handle.
-///
-/// # Safety
-///
-/// Only call this function on an ErrorHandle once.
-#[no_mangle]
-pub unsafe extern "C" fn mun_error_destroy(error: ErrorHandle) {
-    if !error.0.is_null() {
-        let _drop = CString::from_raw(error.0 as *mut c_char);
-    }
-}
diff --git a/crates/mun_runtime_capi/src/field_info.rs b/crates/mun_runtime_capi/src/field_info.rs
deleted file mode 100644
index 8a598b4c3..000000000
--- a/crates/mun_runtime_capi/src/field_info.rs
+++ /dev/null
@@ -1,300 +0,0 @@
-//! Exposes field information using the C ABI.
-
-use crate::{error::ErrorHandle, type_info::TypeInfoHandle};
-use memory::FieldInfo;
-use std::{
-    ffi::{c_void, CString},
-    os::raw::c_char,
-    ptr,
-    sync::Arc,
-};
-
-/// A C-style handle to a `FieldInfo`.
-#[repr(C)]
-#[derive(Clone, Copy)]
-pub struct FieldInfoHandle(pub *const c_void);
-
-impl FieldInfoHandle {
-    /// A null handle.
-    pub fn null() -> Self {
-        Self(ptr::null())
-    }
-}
-
-/// A C-style handle to an array of `FieldInfoHandle`s.
-#[repr(C)]
-pub struct FieldInfoSpan {
-    /// Pointer to the start of the array buffer
-    pub data: *const FieldInfoHandle,
-    /// Length of the array (and capacity)
-    pub len: usize,
-}
-
-impl FieldInfoSpan {
-    /// An empty span.
-    pub fn empty() -> Self {
-        Self {
-            data: ptr::null(),
-            len: 0,
-        }
-    }
-}
-
-/// Retrieves the field's name.
-///
-/// # Safety
-///
-/// The caller is responsible for calling `mun_string_destroy` on the return pointer - if it is not null.
-///
-/// This function might result in undefined behavior if the [`crate::TypeInfoHandle`] associated
-/// with this `FieldInfoHandle` has been deallocated.
-#[no_mangle]
-pub unsafe extern "C" fn mun_field_info_name(field_info: FieldInfoHandle) -> *const c_char {
-    let field_info = match (field_info.0 as *const FieldInfo).as_ref() {
-        Some(field_info) => field_info,
-        None => return ptr::null(),
-    };
-
-    CString::new(field_info.name.clone()).unwrap().into_raw() as *const _
-}
-
-/// Retrieves the field's type.
-///
-/// # Safety
-///
-/// This method is considered unsafe because the passed `field_info` might have been deallocated by
-/// a call to [`mun_type_info_decrement_strong_count`] of the type that contains this field.
-#[no_mangle]
-pub unsafe extern "C" fn mun_field_info_type(field_info: FieldInfoHandle) -> TypeInfoHandle {
-    let field_info = match (field_info.0 as *const FieldInfo).as_ref() {
-        Some(field_info) => field_info,
-        None => return TypeInfoHandle::null(),
-    };
-
-    TypeInfoHandle(Arc::into_raw(field_info.type_info.clone()) as *const c_void)
-}
-
-/// Retrieves the field's offset.
-///
-/// # Safety
-///
-/// This method is considered unsafe because the passed `field_info` might have been deallocated by
-/// a call to [`mun_type_info_decrement_strong_count`] of the type that contains this field.
-#[no_mangle]
-pub unsafe extern "C" fn mun_field_info_offset(
-    field_info: FieldInfoHandle,
-    field_offset: *mut u16,
-) -> ErrorHandle {
-    let field_info = match (field_info.0 as *const FieldInfo).as_ref() {
-        Some(field_info) => field_info,
-        None => return ErrorHandle::new("Invalid argument: 'field_info' is null pointer."),
-    };
-
-    let field_offset = match field_offset.as_mut() {
-        Some(field_offset) => field_offset,
-        None => return ErrorHandle::new("Invalid argument: 'field_offset' is null pointer."),
-    };
-
-    *field_offset = field_info.offset;
-
-    ErrorHandle::default()
-}
-
-/// Deallocates a span of `FieldInfo`s that was allocated by the runtime.
-///
-/// Deallocating span only deallocates the data allocated for the span. Deallocating a span will not
-/// deallocate the FieldInfo's it references. `FieldInfo`s are destroyed when the top-level
-/// `TypeInfo` is destroyed.
-///
-/// # Safety
-///
-/// This function receives a span as parameter. Only when the spans data pointer is not null, is the
-/// content deallocated. Passing pointers to invalid data of memory allocated by other processes,
-/// will lead to undefined behavior.
-#[no_mangle]
-pub unsafe extern "C" fn mun_field_info_span_destroy(span: FieldInfoSpan) -> bool {
-    if span.data.is_null() {
-        return false;
-    }
-
-    let data = span.data as *mut *const FieldInfo;
-    let _types = Vec::from_raw_parts(data, span.len, span.len);
-
-    true
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::{
-        error::mun_error_destroy,
-        mun_string_destroy,
-        runtime::{mun_runtime_get_type_info_by_name, RuntimeHandle},
-        struct_info::mun_struct_info_fields,
-        test_util::TestDriver,
-        type_info::mun_type_info_data,
-        type_info::mun_type_info_eq,
-    };
-    use memory::HasStaticTypeInfo;
-    use std::{
-        ffi::{CStr, CString},
-        mem::MaybeUninit,
-        slice,
-    };
-
-    fn get_first_field<T: Into<Vec<u8>>>(runtime: RuntimeHandle, type_name: T) -> FieldInfoHandle {
-        let type_name = CString::new(type_name).expect("Invalid type name");
-        let mut has_type_info = false;
-        let mut type_info = MaybeUninit::uninit();
-        let handle = unsafe {
-            mun_runtime_get_type_info_by_name(
-                runtime,
-                type_name.as_ptr(),
-                &mut has_type_info as *mut bool,
-                type_info.as_mut_ptr(),
-            )
-        };
-        assert_eq!(handle.0, ptr::null());
-        assert!(has_type_info);
-
-        let type_info = unsafe { type_info.assume_init() };
-
-        let mut type_info_data = MaybeUninit::uninit();
-        let handle = unsafe { mun_type_info_data(type_info, type_info_data.as_mut_ptr()) };
-        assert_eq!(handle.0, ptr::null());
-
-        let type_info_data = unsafe { type_info_data.assume_init() };
-        let struct_info = type_info_data
-            .as_struct()
-            .expect("Type was expected to be a struct.");
-
-        let mut fields = MaybeUninit::uninit();
-        let handle = unsafe { mun_struct_info_fields(struct_info, fields.as_mut_ptr()) };
-        assert_eq!(handle.0, ptr::null());
-
-        let fields = unsafe { fields.assume_init() };
-        assert!(fields.len > 0);
-
-        let field_slice = unsafe { slice::from_raw_parts(fields.data, fields.len) };
-        let first_field = field_slice[0];
-
-        assert!(unsafe { mun_field_info_span_destroy(fields) });
-
-        first_field
-    }
-
-    #[test]
-    fn test_field_info_name_invalid_handle() {
-        let name = unsafe { mun_field_info_name(FieldInfoHandle::null()) };
-        assert_eq!(name, ptr::null());
-    }
-
-    #[test]
-    fn test_field_info_name() {
-        let driver = TestDriver::new(
-            r#"
-            pub struct Foo {
-                a: i32,
-            }
-    "#,
-        );
-
-        let field_info = get_first_field(driver.runtime, "Foo");
-        let name = unsafe { mun_field_info_name(field_info) };
-        assert_ne!(name, ptr::null());
-
-        let name_str = unsafe { CStr::from_ptr(name) }
-            .to_str()
-            .expect("Invalid field name.");
-
-        assert_eq!(name_str, "a");
-
-        unsafe { mun_string_destroy(name) };
-    }
-    #[test]
-    fn test_field_info_type_invalid_handle() {
-        let type_info = unsafe { mun_field_info_type(FieldInfoHandle::null()) };
-        assert_eq!(type_info.0, ptr::null());
-    }
-
-    #[test]
-    fn test_field_info_type() {
-        let driver = TestDriver::new(
-            r#"
-            pub struct Foo {
-                a: i32,
-            }
-    "#,
-        );
-
-        let field_info = get_first_field(driver.runtime, "Foo");
-        let type_info = unsafe { mun_field_info_type(field_info) };
-        assert_ne!(type_info.0, ptr::null());
-        assert!(unsafe { mun_type_info_eq(type_info, i32::type_info().clone().into()) });
-    }
-
-    #[test]
-    fn test_field_info_offset_invalid_field_info() {
-        let mut field_offset = MaybeUninit::uninit();
-        let handle =
-            unsafe { mun_field_info_offset(FieldInfoHandle::null(), field_offset.as_mut_ptr()) };
-        assert_ne!(handle.0, ptr::null());
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'field_info' is null pointer."
-        );
-
-        unsafe { mun_error_destroy(handle) };
-    }
-
-    #[test]
-    fn test_field_info_offset_invalid_field_offset() {
-        let driver = TestDriver::new(
-            r#"
-            pub struct Foo {
-                a: i32,
-            }
-    "#,
-        );
-
-        let field_info = get_first_field(driver.runtime, "Foo");
-
-        let handle = unsafe { mun_field_info_offset(field_info, ptr::null_mut()) };
-        assert_ne!(handle.0, ptr::null());
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'field_offset' is null pointer."
-        );
-
-        unsafe { mun_error_destroy(handle) };
-    }
-
-    #[test]
-    fn test_field_info_offset() {
-        let driver = TestDriver::new(
-            r#"
-            pub struct Foo {
-                a: i32,
-            }
-    "#,
-        );
-
-        let field_info = get_first_field(driver.runtime, "Foo");
-
-        let mut field_offset = MaybeUninit::uninit();
-        let handle = unsafe { mun_field_info_offset(field_info, field_offset.as_mut_ptr()) };
-        assert_eq!(handle.0, ptr::null());
-
-        let field_offset = unsafe { field_offset.assume_init() };
-        assert_eq!(field_offset, 0);
-    }
-
-    #[test]
-    fn test_field_info_span_destroy_empty() {
-        assert!(!unsafe { mun_field_info_span_destroy(FieldInfoSpan::empty()) });
-    }
-}
diff --git a/crates/mun_runtime_capi/src/function.rs b/crates/mun_runtime_capi/src/function.rs
new file mode 100644
index 000000000..0eea55275
--- /dev/null
+++ b/crates/mun_runtime_capi/src/function.rs
@@ -0,0 +1,379 @@
+//! Exposes function information using the C ABI.
+
+use capi_utils::error::ErrorHandle;
+use capi_utils::{mun_error_try, try_deref_mut};
+use memory::ffi::{Type, Types};
+use std::{
+    ffi::{c_void, CString},
+    os::raw::c_char,
+    ptr,
+    sync::Arc,
+};
+
+/// Describes a `Function` accessible from a Mun [`super::runtime::Runtime`].
+///
+/// An instance of `Function` shares ownership of the underlying data. To create a copy of the
+/// `Function` object call [`mun_function_add_reference`] to make sure the number of references to
+/// the data is properly tracked. Calling [`mun_function_release`] signals the runtime that the data
+/// is no longer referenced through the specified object. When all references are released the
+/// underlying data is deallocated.
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct Function(pub *const c_void);
+
+impl Function {
+    /// Returns a Self containing nulls.
+    pub fn null() -> Self {
+        Self(ptr::null())
+    }
+
+    /// Returns a reference to the data that this instance is referencing.
+    ///
+    /// # Safety
+    ///
+    /// The caller must ensure that the internal pointers point to a valid
+    /// [`runtime::FunctionDefinition`].
+    pub unsafe fn inner(&self) -> Result<&runtime::FunctionDefinition, &'static str> {
+        (self.0 as *const runtime::FunctionDefinition)
+            .as_ref()
+            .ok_or("null pointer")
+    }
+}
+
+impl From<Arc<runtime::FunctionDefinition>> for Function {
+    fn from(def: Arc<runtime::FunctionDefinition>) -> Self {
+        Function(Arc::into_raw(def).cast())
+    }
+}
+
+/// Notifies the runtime an additional references exists to the function. This ensures that the data
+/// is kept alive even if [`mun_function_release`] is called for the existing references. Only
+/// after all references have been released can the underlying data be deallocated.
+///
+/// # Safety
+///
+/// This function might be unsafe if the underlying data has already been deallocated by a previous
+/// call to [`mun_function_release`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_function_add_reference(function: Function) -> ErrorHandle {
+    if function.0.is_null() {
+        return ErrorHandle::new("invalid argument 'function': null pointer");
+    }
+
+    Arc::increment_strong_count(function.0);
+    ErrorHandle::default()
+}
+
+/// Notifies the runtime that one of the references to the function is no longer in use. The data
+/// may not immediately be destroyed. Only after all references have been released can the
+/// underlying data be deallocated.
+///
+/// # Safety
+///
+/// This function might be unsafe if the underlying data has been deallocated by a previous call
+/// to [`mun_function_release`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_function_release(function: Function) -> ErrorHandle {
+    if function.0.is_null() {
+        return ErrorHandle::new("invalid argument 'function': null pointer");
+    }
+
+    Arc::decrement_strong_count(function.0);
+    ErrorHandle::default()
+}
+
+/// Retrieves the function's function pointer.
+///
+/// # Safety
+///
+/// This function might be unsafe if the underlying data has been deallocated by a previous call
+/// to [`mun_function_release`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_function_fn_ptr(
+    function: Function,
+    ptr: *mut *const c_void,
+) -> ErrorHandle {
+    let function = mun_error_try!(function
+        .inner()
+        .map_err(|e| format!("invalid argument 'function': {e}")));
+    let ptr = try_deref_mut!(ptr);
+    *ptr = function.fn_ptr;
+    ErrorHandle::default()
+}
+
+/// Retrieves the function's name.
+///
+/// If the function is successful, the caller is responsible for calling [`mun_string_destroy`] on
+/// the return pointer.
+///
+/// # Safety
+///
+/// This function might be unsafe if the underlying data has been deallocated by a previous call
+/// to [`mun_function_release`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_function_name(
+    function: Function,
+    name: *mut *const c_char,
+) -> ErrorHandle {
+    let function = mun_error_try!(function
+        .inner()
+        .map_err(|e| format!("invalid argument 'function': {e}")));
+    let name = try_deref_mut!(name);
+    *name = CString::new(function.prototype.name.clone())
+        .unwrap()
+        .into_raw() as *const _;
+    ErrorHandle::default()
+}
+
+/// Retrieves the function's argument types.
+///
+/// If successful, ownership of the [`Types`] is transferred to the caller. It must be deallocated
+/// with a call to [`mun_types_destroy`].
+///
+/// # Safety
+///
+///
+/// This function might be unsafe if the underlying data has been deallocated by a previous call
+/// to [`mun_function_release`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_function_argument_types(
+    function: Function,
+    arg_types: *mut Types,
+) -> ErrorHandle {
+    let function = mun_error_try!(function
+        .inner()
+        .map_err(|e| format!("invalid argument 'function': {e}")));
+    let arg_types = try_deref_mut!(arg_types);
+    *arg_types = function
+        .prototype
+        .signature
+        .arg_types
+        .iter()
+        .map(|ty| ty.clone().into())
+        .collect::<Vec<_>>()
+        .into();
+    ErrorHandle::default()
+}
+
+/// Retrieves the function's return type.
+///
+/// Ownership of the [`Type`] is transferred to the called. It must be released with a call to
+/// [`mun_type_release`].
+///
+/// # Safety
+///
+/// This function might be unsafe if the underlying data has been deallocated by a previous call
+/// to [`mun_function_release`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_function_return_type(
+    function: Function,
+    ty: *mut Type,
+) -> ErrorHandle {
+    let function = mun_error_try!(function
+        .inner()
+        .map_err(|e| format!("invalid argument 'function': {e}")));
+    let ty = try_deref_mut!(ty);
+    *ty = function.prototype.signature.return_type.clone().into();
+    ErrorHandle::default()
+}
+
+#[cfg(test)]
+pub(crate) mod tests {
+    use super::*;
+    use capi_utils::{
+        assert_error_snapshot, assert_getter1, mun_string_destroy, try_convert_c_string,
+    };
+    use memory::ffi::{mun_type_equal, mun_types_destroy};
+    use memory::HasStaticType;
+    use std::mem::ManuallyDrop;
+    use std::{mem::MaybeUninit, slice, sync::Arc};
+
+    #[test]
+    fn test_function_release_invalid_fn_info() {
+        assert_error_snapshot!(
+            unsafe { mun_function_release(Function::null()) },
+            @r###""invalid argument \'function\': null pointer""###);
+    }
+
+    #[test]
+    fn test_function_add_reference_invalid_fn_info() {
+        assert_error_snapshot!(
+            unsafe { mun_function_add_reference(Function::null()) },
+            @r###""invalid argument \'function\': null pointer""###);
+    }
+
+    #[test]
+    fn test_function_release_strong_count() {
+        let function = runtime::FunctionDefinition::builder("foo").finish();
+        let ffi_function: Function = function.clone().into();
+
+        let fn_def = ManuallyDrop::new(unsafe {
+            Arc::from_raw(ffi_function.0 as *const runtime::FunctionDefinition)
+        });
+        let strong_count = Arc::strong_count(&fn_def);
+        assert!(strong_count > 0);
+
+        assert!(unsafe { mun_function_release(ffi_function) }.is_ok());
+
+        // This works because the Arc is not shared between threads because it's local to the
+        // runtime created in this test
+        assert_eq!(Arc::strong_count(&fn_def), strong_count - 1);
+    }
+
+    #[test]
+    fn test_function_add_reference_strong_count() {
+        let function: Function = runtime::FunctionDefinition::builder("foo").finish().into();
+
+        let fn_info_arc = ManuallyDrop::new(unsafe {
+            Arc::from_raw(function.0 as *const runtime::FunctionDefinition)
+        });
+        let strong_count = Arc::strong_count(&fn_info_arc);
+        assert!(strong_count > 0);
+
+        assert!(unsafe { mun_function_add_reference(function) }.is_ok());
+
+        // This works because the Arc is not shared between threads because it's local to the
+        // runtime created in this test
+        assert_eq!(Arc::strong_count(&fn_info_arc), strong_count + 1);
+    }
+
+    #[test]
+    fn test_function_invalid_fn_info() {
+        let function = runtime::FunctionDefinition::builder("foo").finish().into();
+
+        let mut ptr = MaybeUninit::uninit();
+        assert_error_snapshot!(
+            unsafe { mun_function_fn_ptr(Function::null(), ptr.as_mut_ptr()) },
+            @r###""invalid argument \'function\': null pointer""###);
+        assert_error_snapshot!(
+            unsafe { mun_function_fn_ptr(function, ptr::null_mut()) },
+            @r###""invalid argument \'ptr\': null pointer""###);
+
+        assert!(unsafe { mun_function_release(function) }.is_ok());
+    }
+
+    #[test]
+    fn test_function_fn_ptr() {
+        let invalid_fn_ptr = 0xDEAD as *const c_void;
+        let function = runtime::FunctionDefinition::builder("foo")
+            .set_ptr(invalid_fn_ptr)
+            .finish()
+            .into();
+
+        assert_getter1!(mun_function_fn_ptr(function, fn_ptr));
+        assert_eq!(fn_ptr, invalid_fn_ptr);
+
+        assert!(unsafe { mun_function_release(function) }.is_ok());
+    }
+
+    #[test]
+    fn test_function_name_invalid_fn_info() {
+        let function = runtime::FunctionDefinition::builder("foo").finish().into();
+
+        let mut ptr = MaybeUninit::uninit();
+        assert_error_snapshot!(
+            unsafe { mun_function_name(Function::null(), ptr.as_mut_ptr()) },
+            @r###""invalid argument \'function\': null pointer""###);
+        assert_error_snapshot!(
+            unsafe { mun_function_name(function, ptr::null_mut()) },
+            @r###""invalid argument \'name\': null pointer""###);
+
+        assert!(unsafe { mun_function_release(function) }.is_ok());
+    }
+
+    #[test]
+    fn test_function_name() {
+        let function = runtime::FunctionDefinition::builder("foo").finish().into();
+
+        assert_getter1!(mun_function_name(function, name));
+        assert_ne!(name, ptr::null());
+
+        let name_str = unsafe { try_convert_c_string(name) }.expect("invalid name");
+        assert_eq!(name_str, "foo");
+
+        unsafe { mun_string_destroy(name) };
+        assert!(unsafe { mun_function_release(function) }.is_ok());
+    }
+
+    #[test]
+    fn test_function_argument_types_invalid_fn_info() {
+        let function = runtime::FunctionDefinition::builder("foo").finish().into();
+
+        let mut ptr = MaybeUninit::uninit();
+        assert_error_snapshot!(unsafe {
+            mun_function_argument_types(Function::null(), ptr.as_mut_ptr())
+        }, @r###""invalid argument \'function\': null pointer""###);
+        assert_error_snapshot!(
+            unsafe { mun_function_argument_types(function, ptr::null_mut()) },
+            @r###""invalid argument \'arg_types\': null pointer""###);
+
+        assert!(unsafe { mun_function_release(function) }.is_ok());
+    }
+
+    #[test]
+    fn test_function_argument_types_none() {
+        let function = runtime::FunctionDefinition::builder("foo").finish().into();
+
+        assert_getter1!(mun_function_argument_types(function, arg_types));
+        assert_eq!(arg_types.types, ptr::null());
+        assert_eq!(arg_types.count, 0);
+
+        assert!(unsafe { mun_types_destroy(arg_types) }.is_ok());
+        assert!(unsafe { mun_function_release(function) }.is_ok());
+    }
+
+    #[test]
+    fn test_function_argument_types_some() {
+        let function = runtime::FunctionDefinition::builder("foo")
+            .add_argument(i32::type_info().clone())
+            .add_argument(i32::type_info().clone())
+            .finish()
+            .into();
+
+        assert_getter1!(mun_function_argument_types(function, arg_types));
+        assert_eq!(arg_types.count, 2);
+
+        for arg_type in unsafe { slice::from_raw_parts(arg_types.types, arg_types.count) } {
+            assert!(unsafe { mun_type_equal(*arg_type, i32::type_info().clone().into()) });
+        }
+
+        assert!(unsafe { mun_types_destroy(arg_types) }.is_ok());
+        assert!(unsafe { mun_function_release(function) }.is_ok());
+    }
+
+    #[test]
+    fn test_function_return_type_invalid_fn_info() {
+        let function = runtime::FunctionDefinition::builder("foo").finish().into();
+
+        let mut ptr = MaybeUninit::uninit();
+        assert_error_snapshot!(
+            unsafe { mun_function_return_type(Function::null(), ptr.as_mut_ptr()) },
+            @r###""invalid argument \'function\': null pointer""###);
+        assert_error_snapshot!(
+            unsafe { mun_function_return_type(function, ptr::null_mut()) },
+            @r###""invalid argument \'ty\': null pointer""###);
+
+        assert!(unsafe { mun_function_release(function) }.is_ok());
+    }
+
+    #[test]
+    fn test_function_return_type_none() {
+        let function = runtime::FunctionDefinition::builder("foo").finish().into();
+
+        assert_getter1!(mun_function_return_type(function, return_type));
+
+        assert!(unsafe { mun_type_equal(return_type, <()>::type_info().clone().into()) });
+    }
+
+    #[test]
+    fn test_function_return_type_some() {
+        let function = runtime::FunctionDefinition::builder("foo")
+            .set_return_type(i32::type_info().clone())
+            .finish()
+            .into();
+
+        assert_getter1!(mun_function_return_type(function, return_type));
+
+        assert!(unsafe { mun_type_equal(return_type, i32::type_info().clone().into()) });
+    }
+}
diff --git a/crates/mun_runtime_capi/src/function_info.rs b/crates/mun_runtime_capi/src/function_info.rs
deleted file mode 100644
index ad4e8105f..000000000
--- a/crates/mun_runtime_capi/src/function_info.rs
+++ /dev/null
@@ -1,412 +0,0 @@
-//! Exposes function information using the C ABI.
-
-use crate::{
-    error::ErrorHandle,
-    type_info::{TypeInfoHandle, TypeInfoSpan},
-};
-use memory::TypeInfo;
-use runtime::FunctionDefinition;
-use std::{
-    ffi::{c_void, CString},
-    mem,
-    os::raw::c_char,
-    ptr,
-    sync::Arc,
-};
-
-/// A C-style handle to a `FunctionInfo`.
-#[repr(C)]
-#[derive(Clone, Copy)]
-pub struct FunctionInfoHandle(pub *const c_void);
-
-impl FunctionInfoHandle {
-    /// A null handle.
-    pub fn null() -> Self {
-        Self(ptr::null())
-    }
-}
-
-/// Decrements the strong count of the `Arc<FunctionDefinition>` associated with `handle`.
-///
-/// # Safety
-///
-/// This function might be unsafe if the underlying data has already been deallocated by a previous
-/// call to [`mun_function_info_decrement_strong_count`].
-#[no_mangle]
-pub unsafe extern "C" fn mun_function_info_decrement_strong_count(
-    fn_info: FunctionInfoHandle,
-) -> bool {
-    if !fn_info.0.is_null() {
-        Arc::decrement_strong_count(fn_info.0);
-        return true;
-    }
-
-    false
-}
-
-/// Increments the strong count of the `Arc<FunctionDefinition>` associated with `handle`.
-///
-/// # Safety
-///
-/// This function might be unsafe if the underlying data has been deallocated by a previous call
-/// to [`mun_function_info_decrement_strong_count`].
-#[no_mangle]
-pub unsafe extern "C" fn mun_function_info_increment_strong_count(
-    fn_info: FunctionInfoHandle,
-) -> bool {
-    if !fn_info.0.is_null() {
-        Arc::increment_strong_count(fn_info.0);
-        return true;
-    }
-
-    false
-}
-
-/// Retrieves the function's function pointer.
-///
-/// # Safety
-///
-/// This function might be unsafe if the underlying data has been deallocated by a previous call
-/// to [`mun_function_info_decrement_strong_count`].
-#[no_mangle]
-pub unsafe extern "C" fn mun_function_info_fn_ptr(fn_info: FunctionInfoHandle) -> *const c_void {
-    let fn_def = match (fn_info.0 as *const FunctionDefinition).as_ref() {
-        Some(fn_def) => fn_def,
-        None => return ptr::null(),
-    };
-
-    fn_def.fn_ptr
-}
-
-/// Retrieves the function's name.
-///
-/// # Safety
-///
-/// The caller is responsible for calling `mun_string_destroy` on the return pointer - if it is not null.
-///
-/// This function might be unsafe if the underlying data has been deallocated by a previous call
-/// to [`mun_function_info_decrement_strong_count`].
-#[no_mangle]
-pub unsafe extern "C" fn mun_function_info_name(fn_info: FunctionInfoHandle) -> *const c_char {
-    let fn_def = match (fn_info.0 as *const FunctionDefinition).as_ref() {
-        Some(fn_def) => fn_def,
-        None => return ptr::null(),
-    };
-
-    CString::new(fn_def.prototype.name.clone())
-        .unwrap()
-        .into_raw() as *const _
-}
-
-/// Retrieves the function's argument types.
-///
-/// # Safety
-///
-/// If a non-null handle is returned, the caller is responsible for calling
-/// `mun_type_info_span_destroy` on the returned handle.
-///
-/// This function might be unsafe if the underlying data has been deallocated by a previous call
-/// to [`mun_function_info_decrement_strong_count`].
-#[no_mangle]
-pub unsafe extern "C" fn mun_function_info_argument_types(
-    fn_info: FunctionInfoHandle,
-    arg_types: *mut TypeInfoSpan,
-) -> ErrorHandle {
-    let fn_info = match (fn_info.0 as *const FunctionDefinition).as_ref() {
-        Some(fn_info) => fn_info,
-        None => return ErrorHandle::new("Invalid argument: 'fn_info' is null pointer."),
-    };
-
-    let arg_types = match arg_types.as_mut() {
-        Some(arg_types) => arg_types,
-        None => return ErrorHandle::new("Invalid argument: 'arg_types' is null pointer."),
-    };
-
-    let fn_sig = &fn_info.prototype.signature;
-    let mut types: Vec<*const TypeInfo> = fn_sig
-        .arg_types
-        .iter()
-        .map(|ty| Arc::into_raw(ty.clone()))
-        .collect();
-
-    arg_types.data = if !types.is_empty() {
-        types.shrink_to_fit();
-        types.as_ptr() as *const _
-    } else {
-        ptr::null()
-    };
-    arg_types.len = types.len();
-
-    // Move ownership to caller
-    mem::forget(types);
-
-    ErrorHandle::default()
-}
-
-/// Retrieves the function's return type.
-///
-/// # Safety
-///
-/// This function might be unsafe if the underlying data has been deallocated by a previous call
-/// to [`mun_function_info_decrement_strong_count`].
-#[no_mangle]
-pub unsafe extern "C" fn mun_function_info_return_type(
-    fn_info: FunctionInfoHandle,
-) -> TypeInfoHandle {
-    let fn_info = match (fn_info.0 as *const FunctionDefinition).as_ref() {
-        Some(fn_info) => fn_info,
-        None => return TypeInfoHandle::null(),
-    };
-
-    TypeInfoHandle(Arc::into_raw(fn_info.prototype.signature.return_type.clone()) as *const c_void)
-}
-
-#[cfg(test)]
-pub(crate) mod tests {
-    use super::*;
-    use crate::{
-        error::mun_error_destroy,
-        mun_string_destroy,
-        runtime::{mun_runtime_get_function_info, RuntimeHandle},
-        test_util::TestDriver,
-        type_info::mun_type_info_eq,
-    };
-    use memory::HasStaticTypeInfo;
-    use runtime::FunctionDefinition;
-    use std::{
-        ffi::{CStr, CString},
-        mem::{self, MaybeUninit},
-        sync::Arc,
-    };
-
-    pub(crate) fn get_fake_function_info<T: Into<Vec<u8>>>(
-        runtime: RuntimeHandle,
-        fn_name: T,
-    ) -> FunctionInfoHandle {
-        let fn_name = CString::new(fn_name).expect("Invalid function name");
-        let mut has_fn_info = false;
-        let mut fn_definition = MaybeUninit::uninit();
-        let handle = unsafe {
-            mun_runtime_get_function_info(
-                runtime,
-                fn_name.as_ptr(),
-                &mut has_fn_info as *mut _,
-                fn_definition.as_mut_ptr(),
-            )
-        };
-        assert_eq!(handle.0, ptr::null());
-        assert!(has_fn_info);
-
-        unsafe { fn_definition.assume_init() }
-    }
-
-    #[test]
-    fn test_function_info_decrement_strong_count_invalid_fn_info() {
-        assert!(!unsafe { mun_function_info_decrement_strong_count(FunctionInfoHandle::null()) },);
-    }
-
-    #[test]
-    fn test_function_info_decrement_strong_count() {
-        let driver = TestDriver::new(
-            r#"
-        pub fn main() -> i32 { 12345 }
-    "#,
-        );
-
-        let fn_info = get_fake_function_info(driver.runtime, "main");
-
-        let fn_info_arc = unsafe { Arc::from_raw(fn_info.0 as *const FunctionDefinition) };
-        let strong_count = Arc::strong_count(&fn_info_arc);
-        assert!(strong_count > 0);
-
-        assert!(unsafe { mun_function_info_decrement_strong_count(fn_info) });
-        assert_eq!(Arc::strong_count(&fn_info_arc), strong_count - 1);
-
-        mem::forget(fn_info_arc);
-    }
-
-    #[test]
-    fn test_function_info_increment_strong_count_invalid_fn_info() {
-        assert!(!unsafe { mun_function_info_increment_strong_count(FunctionInfoHandle::null()) });
-    }
-
-    #[test]
-    fn test_function_info_increment_strong_count() {
-        let driver = TestDriver::new(
-            r#"
-        pub fn main() -> i32 { 12345 }
-    "#,
-        );
-
-        let fn_info = get_fake_function_info(driver.runtime, "main");
-
-        let fn_info_arc = unsafe { Arc::from_raw(fn_info.0 as *const FunctionDefinition) };
-        let strong_count = Arc::strong_count(&fn_info_arc);
-        assert!(strong_count > 0);
-
-        assert!(unsafe { mun_function_info_increment_strong_count(fn_info) });
-        assert_eq!(Arc::strong_count(&fn_info_arc), strong_count + 1);
-
-        mem::forget(fn_info_arc);
-    }
-
-    #[test]
-    fn test_function_info_fn_ptr_invalid_fn_info() {
-        let fn_ptr = unsafe { mun_function_info_fn_ptr(FunctionInfoHandle::null()) };
-        assert_eq!(fn_ptr, ptr::null());
-    }
-
-    #[test]
-    fn test_function_info_fn_ptr() {
-        let driver = TestDriver::new(
-            r#"
-        pub fn main() -> i32 { 12345 }
-    "#,
-        );
-
-        let fn_info = get_fake_function_info(driver.runtime, "main");
-        let fn_ptr = unsafe { mun_function_info_fn_ptr(fn_info) };
-        assert_ne!(fn_ptr, ptr::null());
-    }
-
-    #[test]
-    fn test_function_info_name_invalid_fn_info() {
-        let fn_ptr = unsafe { mun_function_info_name(FunctionInfoHandle::null()) };
-        assert_eq!(fn_ptr, ptr::null());
-    }
-
-    #[test]
-    fn test_function_info_name() {
-        let driver = TestDriver::new(
-            r#"
-        pub fn main() -> i32 { 12345 }
-    "#,
-        );
-
-        let fn_info = get_fake_function_info(driver.runtime, "main");
-        let name = unsafe { mun_function_info_name(fn_info) };
-        assert_ne!(name, ptr::null());
-
-        let name_str = unsafe { CStr::from_ptr(name) }
-            .to_str()
-            .expect("Invalid function name.");
-
-        assert_eq!(name_str, "main");
-
-        unsafe { mun_string_destroy(name) };
-    }
-
-    #[test]
-    fn test_function_info_argument_types_invalid_fn_info() {
-        let handle = unsafe {
-            mun_function_info_argument_types(FunctionInfoHandle::null(), ptr::null_mut())
-        };
-        assert_ne!(handle.0, ptr::null());
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'fn_info' is null pointer."
-        );
-
-        unsafe { mun_error_destroy(handle) };
-    }
-
-    #[test]
-    fn test_function_info_argument_types_invalid_arg_types() {
-        let driver = TestDriver::new(
-            r#"
-        pub fn main() -> i32 { 12345 }
-    "#,
-        );
-
-        let fn_info = get_fake_function_info(driver.runtime, "main");
-        let handle = unsafe { mun_function_info_argument_types(fn_info, ptr::null_mut()) };
-        assert_ne!(handle.0, ptr::null());
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'arg_types' is null pointer."
-        );
-
-        unsafe { mun_error_destroy(handle) };
-    }
-
-    #[test]
-    fn test_function_info_argument_types_none() {
-        let driver = TestDriver::new(
-            r#"
-        pub fn main() -> i32 { 12345 }
-    "#,
-        );
-
-        let fn_info = get_fake_function_info(driver.runtime, "main");
-        let mut arg_types = MaybeUninit::uninit();
-        let handle = unsafe { mun_function_info_argument_types(fn_info, arg_types.as_mut_ptr()) };
-        assert_eq!(handle.0, ptr::null());
-
-        let arg_types = unsafe { arg_types.assume_init() };
-        assert_eq!(arg_types.data, ptr::null());
-        assert_eq!(arg_types.len, 0);
-    }
-
-    #[test]
-    fn test_function_info_argument_types_some() {
-        let driver = TestDriver::new(
-            r#"
-        pub fn add(a: i32, b: i32) -> i32 { a + b }
-    "#,
-        );
-
-        let fn_info = get_fake_function_info(driver.runtime, "add");
-        let mut arg_types = MaybeUninit::uninit();
-        let handle = unsafe { mun_function_info_argument_types(fn_info, arg_types.as_mut_ptr()) };
-        assert_eq!(handle.0, ptr::null());
-
-        let arg_types = unsafe { arg_types.assume_init() };
-        assert_eq!(arg_types.len, 2);
-
-        (0..arg_types.len).into_iter().for_each(|index| {
-            let type_info = arg_types.get(index);
-            assert_ne!(type_info.0, ptr::null());
-            assert!(unsafe { mun_type_info_eq(type_info, i32::type_info().clone().into()) });
-        })
-    }
-
-    #[test]
-    fn test_function_info_return_type_invalid_fn_info() {
-        let return_type = unsafe { mun_function_info_return_type(FunctionInfoHandle::null()) };
-        assert_eq!(return_type.0, ptr::null());
-    }
-
-    #[test]
-    fn test_function_info_return_type_none() {
-        let driver = TestDriver::new(
-            r#"
-        pub fn main() { }
-    "#,
-        );
-
-        let fn_info = get_fake_function_info(driver.runtime, "main");
-        let return_type = unsafe { mun_function_info_return_type(fn_info) };
-        assert_ne!(return_type.0, ptr::null());
-
-        assert!(unsafe { mun_type_info_eq(return_type, <()>::type_info().clone().into()) });
-    }
-
-    #[test]
-    fn test_function_info_return_type_some() {
-        let driver = TestDriver::new(
-            r#"
-        pub fn main() -> i32 { 12345 }
-    "#,
-        );
-
-        let fn_info = get_fake_function_info(driver.runtime, "main");
-        let return_type = unsafe { mun_function_info_return_type(fn_info) };
-        assert_ne!(return_type.0, ptr::null());
-
-        assert!(unsafe { mun_type_info_eq(return_type, <i32>::type_info().clone().into()) });
-    }
-}
diff --git a/crates/mun_runtime_capi/src/gc.rs b/crates/mun_runtime_capi/src/gc.rs
index f6d9bccc2..8d9d483fd 100644
--- a/crates/mun_runtime_capi/src/gc.rs
+++ b/crates/mun_runtime_capi/src/gc.rs
@@ -1,14 +1,15 @@
 //! Exposes Mun garbage collection.
 
-use std::{ffi::c_void, sync::Arc};
+use capi_utils::error::ErrorHandle;
+use capi_utils::{mun_error_try, try_deref_mut};
+use std::mem::ManuallyDrop;
 
-use crate::{error::ErrorHandle, runtime::RuntimeHandle, type_info::TypeInfoHandle};
-use memory::TypeInfo;
-use runtime::Runtime;
+use crate::runtime::Runtime;
+use memory::ffi::Type;
 
 pub use memory::gc::GcPtr;
 
-/// Allocates an object in the runtime of the given `type_info`. If successful, `obj` is set,
+/// Allocates an object in the runtime of the given `ty`. If successful, `obj` is set,
 /// otherwise a non-zero error handle is returned.
 ///
 /// If a non-zero error handle is returned, it must be manually destructed using
@@ -19,36 +20,21 @@ pub use memory::gc::GcPtr;
 /// This function receives raw pointers as parameters. If any of the arguments is a null pointer,
 /// an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
 #[no_mangle]
-pub unsafe extern "C" fn mun_gc_alloc(
-    runtime: RuntimeHandle,
-    type_info: TypeInfoHandle,
-    obj: *mut GcPtr,
-) -> ErrorHandle {
-    let runtime = match (runtime.0 as *mut Runtime).as_ref() {
-        Some(runtime) => runtime,
-        None => return ErrorHandle::new("Invalid argument: 'runtime' is null pointer."),
-    };
-
-    if type_info.0.is_null() {
-        return ErrorHandle::new("Invalid argument: 'type_info' is null pointer.");
-    }
-
-    let obj = match obj.as_mut() {
-        Some(obj) => obj,
-        None => return ErrorHandle::new("Invalid argument: 'obj' is null pointer."),
-    };
-
-    let type_info = Arc::from_raw(type_info.0 as *const TypeInfo);
-
-    *obj = runtime.gc().alloc(&type_info);
-
-    std::mem::forget(type_info);
-
+pub unsafe extern "C" fn mun_gc_alloc(runtime: Runtime, ty: Type, obj: *mut GcPtr) -> ErrorHandle {
+    let runtime = mun_error_try!(runtime
+        .inner()
+        .map_err(|e| format!("invalid argument 'runtime': {e}")));
+    let ty = mun_error_try!(ty
+        .to_owned()
+        .map_err(|e| format!("invalid argument 'obj': {e}"))
+        .map(ManuallyDrop::new));
+    let obj = try_deref_mut!(obj);
+    *obj = runtime.gc().alloc(&ty);
     ErrorHandle::default()
 }
 
-/// Retrieves the `type_info` for the specified `obj` from the runtime. If successful, `type_info`
-/// is set, otherwise a non-zero error handle is returned.
+/// Retrieves the `ty` for the specified `obj` from the runtime. If successful, `ty` is set,
+/// otherwise a non-zero error handle is returned.
 ///
 /// If a non-zero error handle is returned, it must be manually destructed using
 /// [`mun_error_destroy`].
@@ -59,22 +45,15 @@ pub unsafe extern "C" fn mun_gc_alloc(
 /// an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
 #[no_mangle]
 pub unsafe extern "C" fn mun_gc_ptr_type(
-    handle: RuntimeHandle,
+    runtime: Runtime,
     obj: GcPtr,
-    type_info: *mut TypeInfoHandle,
+    ty: *mut Type,
 ) -> ErrorHandle {
-    let runtime = match (handle.0 as *mut Runtime).as_ref() {
-        Some(runtime) => runtime,
-        None => return ErrorHandle::new("Invalid argument: 'runtime' is null pointer."),
-    };
-
-    let type_info = match type_info.as_mut() {
-        Some(type_info) => type_info,
-        None => return ErrorHandle::new("Invalid argument: 'type_info' is null pointer."),
-    };
-
-    type_info.0 = Arc::into_raw(runtime.gc().ptr_type(obj)) as *const c_void;
-
+    let runtime = mun_error_try!(runtime
+        .inner()
+        .map_err(|e| format!("invalid argument 'runtime': {e}")));
+    let ty = try_deref_mut!(ty);
+    *ty = runtime.gc().ptr_type(obj).into();
     ErrorHandle::default()
 }
 
@@ -92,12 +71,10 @@ pub unsafe extern "C" fn mun_gc_ptr_type(
 /// This function receives raw pointers as parameters. If any of the arguments is a null pointer,
 /// an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
 #[no_mangle]
-pub unsafe extern "C" fn mun_gc_root(handle: RuntimeHandle, obj: GcPtr) -> ErrorHandle {
-    let runtime = match (handle.0 as *mut Runtime).as_ref() {
-        Some(runtime) => runtime,
-        None => return ErrorHandle::new("Invalid argument: 'runtime' is null pointer."),
-    };
-
+pub unsafe extern "C" fn mun_gc_root(runtime: Runtime, obj: GcPtr) -> ErrorHandle {
+    let runtime = mun_error_try!(runtime
+        .inner()
+        .map_err(|e| format!("invalid argument 'runtime': {e}")));
     runtime.gc().root(obj);
     ErrorHandle::default()
 }
@@ -115,12 +92,10 @@ pub unsafe extern "C" fn mun_gc_root(handle: RuntimeHandle, obj: GcPtr) -> Error
 /// This function receives raw pointers as parameters. If any of the arguments is a null pointer,
 /// an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
 #[no_mangle]
-pub unsafe extern "C" fn mun_gc_unroot(handle: RuntimeHandle, obj: GcPtr) -> ErrorHandle {
-    let runtime = match (handle.0 as *mut Runtime).as_ref() {
-        Some(runtime) => runtime,
-        None => return ErrorHandle::new("Invalid argument: 'runtime' is null pointer."),
-    };
-
+pub unsafe extern "C" fn mun_gc_unroot(runtime: Runtime, obj: GcPtr) -> ErrorHandle {
+    let runtime = mun_error_try!(runtime
+        .inner()
+        .map_err(|e| format!("invalid argument 'runtime': {e}")));
     runtime.gc().unroot(obj);
     ErrorHandle::default()
 }
@@ -137,41 +112,34 @@ pub unsafe extern "C" fn mun_gc_unroot(handle: RuntimeHandle, obj: GcPtr) -> Err
 /// This function receives raw pointers as parameters. If any of the arguments is a null pointer,
 /// an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
 #[no_mangle]
-pub unsafe extern "C" fn mun_gc_collect(
-    handle: RuntimeHandle,
-    reclaimed: *mut bool,
-) -> ErrorHandle {
-    let runtime = match (handle.0 as *mut Runtime).as_ref() {
-        Some(runtime) => runtime,
-        None => return ErrorHandle::new("Invalid argument: 'runtime' is null pointer."),
-    };
-
-    let reclaimed = match reclaimed.as_mut() {
-        Some(reclaimed) => reclaimed,
-        None => return ErrorHandle::new("Invalid argument: 'reclaimed' is null pointer."),
-    };
-
+pub unsafe extern "C" fn mun_gc_collect(runtime: Runtime, reclaimed: *mut bool) -> ErrorHandle {
+    let runtime = mun_error_try!(runtime
+        .inner()
+        .map_err(|e| format!("invalid argument 'runtime': {e}")));
+    let reclaimed = try_deref_mut!(reclaimed);
     *reclaimed = runtime.gc_collect();
     ErrorHandle::default()
 }
 
 #[cfg(test)]
 mod tests {
+    use memory::ffi::{mun_type_equal, Type};
     use memory::gc::{HasIndirectionPtr, RawGcPtr};
 
     use super::*;
     use crate::{
-        error::mun_error_destroy, runtime::mun_runtime_get_type_info_by_name, test_invalid_runtime,
-        test_util::TestDriver,
+        runtime::mun_runtime_get_type_info_by_name, test_invalid_runtime, test_util::TestDriver,
     };
+    use capi_utils::error::mun_error_destroy;
+    use capi_utils::{assert_error_snapshot, assert_getter1, assert_getter2};
     use std::{
-        ffi::{CStr, CString},
-        mem::{self, MaybeUninit},
+        ffi::CString,
+        mem::{self},
         ptr,
     };
 
     test_invalid_runtime!(
-        gc_alloc(TypeInfoHandle::null(), ptr::null_mut()),
+        gc_alloc(Type::null(), ptr::null_mut()),
         gc_ptr_type(mem::zeroed::<GcPtr>(), ptr::null_mut()),
         gc_root(mem::zeroed::<GcPtr>()),
         gc_unroot(mem::zeroed::<GcPtr>()),
@@ -186,17 +154,10 @@ mod tests {
     "#,
         );
 
-        let handle =
-            unsafe { mun_gc_alloc(driver.runtime, TypeInfoHandle::null(), ptr::null_mut()) };
-        assert_ne!(handle.0, ptr::null());
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'type_info' is null pointer."
+        assert_error_snapshot!(
+            unsafe { mun_gc_alloc(driver.runtime, Type::null(), ptr::null_mut()) },
+            @r###""invalid argument \'obj\': null pointer""###
         );
-
-        unsafe { mun_error_destroy(handle) };
     }
 
     #[test]
@@ -208,29 +169,18 @@ mod tests {
         );
 
         let type_name = CString::new("Foo").expect("Invalid type name.");
-        let mut has_type = false;
-        let mut type_info = TypeInfoHandle::null();
-
-        let handle = unsafe {
-            mun_runtime_get_type_info_by_name(
-                driver.runtime,
-                type_name.as_ptr(),
-                &mut has_type as *mut bool,
-                &mut type_info as *mut TypeInfoHandle,
-            )
-        };
-        assert_eq!(handle.0, ptr::null());
-
-        let handle = unsafe { mun_gc_alloc(driver.runtime, type_info, ptr::null_mut()) };
-        assert_ne!(handle.0, ptr::null());
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'obj' is null pointer."
+        assert_getter2!(mun_runtime_get_type_info_by_name(
+            driver.runtime,
+            type_name.as_ptr(),
+            has_type,
+            ty,
+        ));
+        assert!(has_type);
+
+        assert_error_snapshot!(
+            unsafe { mun_gc_alloc(driver.runtime, ty, ptr::null_mut()) },
+            @r###""invalid argument \'obj\': null pointer""###
         );
-
-        unsafe { mun_error_destroy(handle) };
     }
 
     #[test]
@@ -242,29 +192,19 @@ mod tests {
         );
 
         let type_name = CString::new("Foo").expect("Invalid type name.");
-        let mut has_type = false;
-        let mut type_info = TypeInfoHandle::null();
-
-        let handle = unsafe {
-            mun_runtime_get_type_info_by_name(
-                driver.runtime,
-                type_name.as_ptr(),
-                &mut has_type as *mut bool,
-                &mut type_info as *mut TypeInfoHandle,
-            )
-        };
-        assert_eq!(handle.0, ptr::null());
-
-        let mut obj = MaybeUninit::uninit();
-        let handle = unsafe { mun_gc_alloc(driver.runtime, type_info, obj.as_mut_ptr()) };
-        assert_eq!(handle.0, ptr::null());
-
-        let obj = unsafe { obj.assume_init() };
+        assert_getter2!(mun_runtime_get_type_info_by_name(
+            driver.runtime,
+            type_name.as_ptr(),
+            has_type,
+            ty,
+        ));
+        assert!(has_type);
+
+        assert_getter2!(mun_gc_alloc(driver.runtime, ty, obj));
         assert_ne!(unsafe { obj.deref::<u8>() }, ptr::null());
 
-        let mut reclaimed = false;
-        let handle = unsafe { mun_gc_collect(driver.runtime, &mut reclaimed as *mut _) };
-        assert_eq!(handle.0, ptr::null());
+        assert_getter1!(mun_gc_collect(driver.runtime, reclaimed));
+        assert!(reclaimed);
     }
 
     #[test]
@@ -275,18 +215,13 @@ mod tests {
     "#,
         );
 
-        let handle = unsafe {
-            let raw_ptr: RawGcPtr = ptr::null();
-            mun_gc_ptr_type(driver.runtime, raw_ptr.into(), ptr::null_mut())
-        };
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'type_info' is null pointer."
+        assert_error_snapshot!(
+            unsafe {
+                let raw_ptr: RawGcPtr = ptr::null();
+                mun_gc_ptr_type(driver.runtime, raw_ptr.into(), ptr::null_mut())
+            },
+            @r###""invalid argument \'ty\': null pointer""###
         );
-
-        unsafe { mun_error_destroy(handle) };
     }
 
     #[test]
@@ -298,36 +233,21 @@ mod tests {
         );
 
         let type_name = CString::new("Foo").expect("Invalid type name.");
-        let mut has_type = false;
-        let mut type_info = TypeInfoHandle::null();
-
-        let handle = unsafe {
-            mun_runtime_get_type_info_by_name(
-                driver.runtime,
-                type_name.as_ptr(),
-                &mut has_type as *mut bool,
-                &mut type_info as *mut TypeInfoHandle,
-            )
-        };
-        assert_eq!(handle.0, ptr::null());
-
-        let mut obj = MaybeUninit::uninit();
-        let handle = unsafe { mun_gc_alloc(driver.runtime, type_info, obj.as_mut_ptr()) };
-        assert_eq!(handle.0, ptr::null());
-
-        let obj = unsafe { obj.assume_init() };
+        assert_getter2!(mun_runtime_get_type_info_by_name(
+            driver.runtime,
+            type_name.as_ptr(),
+            has_type,
+            ty,
+        ));
+        assert!(has_type);
+
+        assert_getter2!(mun_gc_alloc(driver.runtime, ty, obj));
         assert_ne!(unsafe { obj.deref::<u8>() }, ptr::null());
 
-        let mut ty = MaybeUninit::uninit();
-        let handle = unsafe { mun_gc_ptr_type(driver.runtime, obj, ty.as_mut_ptr()) };
-        assert_eq!(handle.0, ptr::null());
+        assert_getter2!(mun_gc_ptr_type(driver.runtime, obj, ptr_ty));
+        assert!(unsafe { mun_type_equal(ty, ptr_ty) });
 
-        let ty = unsafe { ty.assume_init() };
-        assert_eq!(type_info.0, ty.0);
-
-        let mut reclaimed = false;
-        let handle = unsafe { mun_gc_collect(driver.runtime, &mut reclaimed as *mut _) };
-        assert_eq!(handle.0, ptr::null());
+        assert_getter1!(mun_gc_collect(driver.runtime, reclaimed));
         assert!(reclaimed);
     }
 
@@ -340,40 +260,25 @@ mod tests {
         );
 
         let type_name = CString::new("Foo").expect("Invalid type name.");
-        let mut has_type = false;
-        let mut type_info = TypeInfoHandle::null();
-
-        let handle = unsafe {
-            mun_runtime_get_type_info_by_name(
-                driver.runtime,
-                type_name.as_ptr(),
-                &mut has_type as *mut bool,
-                &mut type_info as *mut TypeInfoHandle,
-            )
-        };
-        assert_eq!(handle.0, ptr::null());
-
-        let mut obj = MaybeUninit::uninit();
-        let handle = unsafe { mun_gc_alloc(driver.runtime, type_info, obj.as_mut_ptr()) };
-        assert_eq!(handle.0, ptr::null());
-
-        let obj = unsafe { obj.assume_init() };
+        assert_getter2!(mun_runtime_get_type_info_by_name(
+            driver.runtime,
+            type_name.as_ptr(),
+            has_type,
+            ty,
+        ));
+        assert!(has_type);
+
+        assert_getter2!(mun_gc_alloc(driver.runtime, ty, obj));
         assert_ne!(unsafe { obj.deref::<u8>() }, ptr::null());
 
-        let handle = unsafe { mun_gc_root(driver.runtime, obj) };
-
-        assert_eq!(handle.0, ptr::null());
+        assert!(unsafe { mun_gc_root(driver.runtime, obj) }.is_ok());
 
-        let mut reclaimed = false;
-        let handle = unsafe { mun_gc_collect(driver.runtime, &mut reclaimed as *mut _) };
-        assert_eq!(handle.0, ptr::null());
+        assert_getter1!(mun_gc_collect(driver.runtime, reclaimed));
         assert!(!reclaimed);
 
-        let handle = unsafe { mun_gc_unroot(driver.runtime, obj) };
-        assert_eq!(handle.0, ptr::null());
+        assert!(unsafe { mun_gc_unroot(driver.runtime, obj) }.is_ok());
 
-        let handle = unsafe { mun_gc_collect(driver.runtime, &mut reclaimed as *mut _) };
-        assert_eq!(handle.0, ptr::null());
+        assert_getter1!(mun_gc_collect(driver.runtime, reclaimed));
         assert!(reclaimed);
     }
 
@@ -387,14 +292,9 @@ mod tests {
     "#,
         );
 
-        let handle = unsafe { mun_gc_collect(driver.runtime, ptr::null_mut()) };
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'reclaimed' is null pointer."
+        assert_error_snapshot!(
+            unsafe { mun_gc_collect(driver.runtime, ptr::null_mut()) },
+            @r###""invalid argument \'reclaimed\': null pointer""###
         );
-
-        unsafe { mun_error_destroy(handle) };
     }
 }
diff --git a/crates/mun_runtime_capi/src/lib.rs b/crates/mun_runtime_capi/src/lib.rs
index e5bc25b8c..9c23034a1 100644
--- a/crates/mun_runtime_capi/src/lib.rs
+++ b/crates/mun_runtime_capi/src/lib.rs
@@ -4,32 +4,11 @@
 //! integrate the Mun Runtime into other languages that allow interoperability with C.
 #![warn(missing_docs)]
 
-pub mod error;
 pub mod gc;
 pub mod runtime;
 
-pub mod field_info;
-pub mod function_info;
-pub mod struct_info;
-pub mod type_info;
+pub mod function;
 
 #[macro_use]
 #[cfg(test)]
 mod test_util;
-
-use std::{ffi::CString, os::raw::c_char};
-
-/// Deallocates a string that was allocated by the runtime.
-///
-/// # Safety
-///
-/// This function receives a raw pointer as parameter. Only when the argument is not a null pointer,
-/// its content will be deallocated. Passing pointers to invalid data or memory allocated by other
-/// processes, will lead to undefined behavior.
-#[no_mangle]
-pub unsafe extern "C" fn mun_string_destroy(string: *const c_char) {
-    if !string.is_null() {
-        // Destroy the string
-        let _string = CString::from_raw(string as *mut _);
-    }
-}
diff --git a/crates/mun_runtime_capi/src/runtime.rs b/crates/mun_runtime_capi/src/runtime.rs
index 379109049..8485a4d8d 100644
--- a/crates/mun_runtime_capi/src/runtime.rs
+++ b/crates/mun_runtime_capi/src/runtime.rs
@@ -1,20 +1,43 @@
 //! Exposes the Mun runtime using the C ABI.
 
-use memory::type_table::TypeTable;
-use memory::TypeInfo;
-use runtime::{FunctionDefinition, FunctionPrototype, FunctionSignature, Runtime};
-use std::{
-    ffi::{c_void, CStr},
-    os::raw::c_char,
-    sync::Arc,
+use capi_utils::{
+    error::ErrorHandle, mun_error_try, try_convert_c_string, try_deref, try_deref_mut,
 };
+use memory::{ffi::Type, type_table::TypeTable, Type as RustType};
+use runtime::{FunctionDefinition, FunctionPrototype, FunctionSignature};
+use std::{ffi::c_void, mem::ManuallyDrop, ops::Deref, os::raw::c_char, slice};
 
-use crate::{error::ErrorHandle, function_info::FunctionInfoHandle, type_info::TypeInfoHandle};
+use crate::function::Function;
 
 /// A C-style handle to a runtime.
 #[repr(C)]
 #[derive(Clone, Copy)]
-pub struct RuntimeHandle(pub *mut c_void);
+pub struct Runtime(pub *mut c_void);
+
+impl Runtime {
+    /// Returns a reference to rust Runtime, or an error if this instance contains a null pointer.
+    ///
+    /// # Safety
+    ///
+    /// The caller must ensure that the internal pointers point to a valid [`runtime::Runtime`].
+    pub(crate) unsafe fn inner(&self) -> Result<&runtime::Runtime, &'static str> {
+        (self.0 as *mut runtime::Runtime)
+            .as_ref()
+            .ok_or("null pointer")
+    }
+
+    /// Returns a mutable reference to rust Runtime, or an error if this instance contains a null
+    /// pointer.
+    ///
+    /// # Safety
+    ///
+    /// The caller must ensure that the internal pointers point to a valid [`runtime::Runtime`].
+    pub unsafe fn inner_mut(&self) -> Result<&mut runtime::Runtime, &'static str> {
+        (self.0 as *mut runtime::Runtime)
+            .as_mut()
+            .ok_or("null pointer")
+    }
+}
 
 /// Definition of an external function that is callable from Mun.
 ///
@@ -29,10 +52,10 @@ pub struct ExternalFunctionDefinition {
     pub num_args: u32,
 
     /// The types of the arguments
-    pub arg_types: *const TypeInfoHandle,
+    pub arg_types: *const Type,
 
     /// The type of the return type
-    pub return_type: TypeInfoHandle,
+    pub return_type: Type,
 
     /// Pointer to the function
     pub fn_ptr: *const c_void,
@@ -83,108 +106,77 @@ impl Default for RuntimeOptions {
 pub unsafe extern "C" fn mun_runtime_create(
     library_path: *const c_char,
     options: RuntimeOptions,
-    handle: *mut RuntimeHandle,
+    handle: *mut Runtime,
 ) -> ErrorHandle {
-    if library_path.is_null() {
-        return ErrorHandle::new("Invalid argument: 'library_path' is null pointer.");
-    }
+    let library_path = mun_error_try!(try_convert_c_string(library_path)
+        .map_err(|e| format!("invalid argument 'library_path': {e}")));
+    let handle = try_deref_mut!(handle);
 
     if options.num_functions > 0 && options.functions.is_null() {
-        return ErrorHandle::new("Invalid argument: 'functions' is null pointer.");
+        return ErrorHandle::new("invalid argument: 'functions' is null pointer.");
     }
 
-    let library_path = match CStr::from_ptr(library_path).to_str() {
-        Ok(path) => path,
-        Err(_) => {
-            return ErrorHandle::new("Invalid argument: 'library_path' is not UTF-8 encoded.");
+    let type_table = TypeTable::default();
+    let user_functions = mun_error_try!(std::slice::from_raw_parts(
+        options.functions,
+        options.num_functions as usize
+    )
+    .iter()
+    .map(|def| {
+        let name =
+            try_convert_c_string(def.name).map_err(|e| format!("invalid function name: {e}"))?;
+        let return_type = ManuallyDrop::new(
+            def.return_type
+                .to_owned()
+                .map_err(|e| format!("invalid function '{name}': 'return_type': {e}"))?,
+        )
+        .deref()
+        .clone();
+
+        if def.num_args > 0 && def.arg_types.is_null() {
+            return Err(format!(
+                "invalid function '{}': 'arg_types' is null pointer.",
+                name
+            ));
         }
-    };
 
-    let handle = match handle.as_mut() {
-        Some(handle) => handle,
-        None => return ErrorHandle::new("Invalid argument: 'handle' is null pointer."),
-    };
-
-    let type_table = TypeTable::default();
-    let user_functions =
-        match std::slice::from_raw_parts(options.functions, options.num_functions as usize)
-            .iter()
-            .map(|def| {
-                if def.name.is_null() {
-                    return Err(String::from(
-                        "Invalid function name: function name null pointer",
-                    ));
-                }
-
-                let name = match CStr::from_ptr(def.name).to_str() {
-                    Ok(path) => path,
-                    Err(e) => {
-                        return Err(format!(
-                            "Invalid function name: function name is not UTF-8 encoded: {}",
-                            e
-                        ));
-                    }
-                };
-
-                if def.return_type.0.is_null() {
-                    return Err(format!(
-                        "Invalid function '{}': 'return_type' is null pointer.",
-                        name
-                    ));
-                }
-
-                if def.num_args > 0 && def.arg_types.is_null() {
-                    return Err(format!(
-                        "Invalid function '{}': 'arg_types' is null pointer.",
-                        name
-                    ));
-                }
-
-                let arg_types: Vec<Arc<TypeInfo>> = if def.num_args > 0 {
-                    std::slice::from_raw_parts(def.arg_types, def.num_args as usize)
-                        .iter()
-                        .enumerate()
-                        .map(|(i, arg)| {
-                            if arg.0.is_null() {
-                                return Err(format!(
-                                    "Invalid function '{}': argument #{} is null pointer.",
-                                    name,
-                                    i + 1
-                                ));
-                            }
-                            Ok(Arc::from_raw(arg.0 as _))
-                        })
-                        .collect::<Result<_, _>>()?
-                } else {
-                    Vec::new()
-                };
-
-                Ok(FunctionDefinition {
-                    prototype: FunctionPrototype {
-                        name: name.to_owned(),
-                        signature: FunctionSignature {
-                            arg_types,
-                            return_type: Arc::from_raw(def.return_type.0 as _),
-                        },
-                    },
-                    fn_ptr: def.fn_ptr,
+        let arg_types: Vec<_> = if def.num_args > 0 {
+            std::slice::from_raw_parts(def.arg_types, def.num_args as usize)
+                .iter()
+                .enumerate()
+                .map(|(i, arg)| -> Result<RustType, String> {
+                    let ty = (*arg).to_owned().map_err(|e| {
+                        format!("invalid function '{}': argument #{}: {}", name, i + 1, e)
+                    })?;
+                    Ok(ManuallyDrop::new(ty).deref().clone())
                 })
-            })
-            .collect::<Result<_, _>>()
-        {
-            Err(e) => return ErrorHandle::new(e),
-            Ok(user_functions) => user_functions,
+                .collect::<Result<_, _>>()?
+        } else {
+            Vec::new()
         };
 
+        Ok(FunctionDefinition {
+            prototype: FunctionPrototype {
+                name: name.to_owned(),
+                signature: FunctionSignature {
+                    arg_types,
+                    return_type,
+                },
+            },
+            fn_ptr: def.fn_ptr,
+        })
+    })
+    .collect::<Result<_, _>>());
+
     let runtime_options = runtime::RuntimeOptions {
         library_path: library_path.into(),
         user_functions,
         type_table,
     };
 
-    let runtime = match Runtime::new(runtime_options) {
+    let runtime = match runtime::Runtime::new(runtime_options) {
         Ok(runtime) => runtime,
-        Err(e) => return ErrorHandle::new(e.to_string()),
+        Err(e) => return ErrorHandle::new(format!("{:?}", e)),
     };
 
     handle.0 = Box::into_raw(Box::new(runtime)) as *mut _;
@@ -193,15 +185,16 @@ pub unsafe extern "C" fn mun_runtime_create(
 
 /// Destructs the runtime corresponding to `handle`.
 #[no_mangle]
-pub extern "C" fn mun_runtime_destroy(handle: RuntimeHandle) {
-    if !handle.0.is_null() {
-        let _runtime = unsafe { Box::from_raw(handle.0) };
+pub extern "C" fn mun_runtime_destroy(runtime: Runtime) -> ErrorHandle {
+    if runtime.0.is_null() {
+        return ErrorHandle::new("invalid argument 'runtime': null pointer");
     }
+    let _runtime = unsafe { Box::from_raw(runtime.0) };
+    ErrorHandle::default()
 }
 
-/// Retrieves the [`FunctionDefinition`] for `fn_name` from the runtime corresponding to `handle`.
-/// If successful, `has_fn_info` and `fn_info` are set, otherwise a non-zero error handle is
-/// returned.
+/// Retrieves the [`FunctionDefinition`] for `fn_name` from the `runtime`. If successful,
+/// `has_fn_info` and `fn_info` are set, otherwise a non-zero error handle is returned.
 ///
 /// If a non-zero error handle is returned, it must be manually destructed using
 /// [`mun_error_destroy`].
@@ -211,42 +204,30 @@ pub extern "C" fn mun_runtime_destroy(handle: RuntimeHandle) {
 /// This function receives raw pointers as parameters. If any of the arguments is a null pointer,
 /// an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
 #[no_mangle]
-pub unsafe extern "C" fn mun_runtime_get_function_info(
-    handle: RuntimeHandle,
+pub unsafe extern "C" fn mun_runtime_find_function_definition(
+    runtime: Runtime,
     fn_name: *const c_char,
+    fn_name_len: usize,
     has_fn_info: *mut bool,
-    fn_info: *mut FunctionInfoHandle,
+    fn_info: *mut Function,
 ) -> ErrorHandle {
-    let runtime = match (handle.0 as *mut Runtime).as_ref() {
-        Some(runtime) => runtime,
-        None => return ErrorHandle::new("Invalid argument: 'runtime' is null pointer."),
-    };
-
+    let runtime = mun_error_try!(runtime
+        .inner()
+        .map_err(|e| format!("invalid argument 'runtime': {e}")));
     if fn_name.is_null() {
-        return ErrorHandle::new("Invalid argument: 'fn_name' is null pointer.");
-    }
-
-    let fn_name = match CStr::from_ptr(fn_name).to_str() {
-        Ok(name) => name,
-        Err(_) => {
-            return ErrorHandle::new("Invalid argument: 'fn_name' is not UTF-8 encoded.");
-        }
-    };
-
-    let has_fn_info = match has_fn_info.as_mut() {
-        Some(has_info) => has_info,
-        None => return ErrorHandle::new("Invalid argument: 'has_fn_info' is null pointer."),
-    };
-
-    let fn_info = match fn_info.as_mut() {
-        Some(fn_info) => fn_info,
-        None => return ErrorHandle::new("Invalid argument: 'fn_info' is null pointer."),
-    };
-
-    match runtime.get_function_definition(fn_name) {
+        return ErrorHandle::new("invalid argument 'fn_name': null pointer");
+    }
+    let name = mun_error_try!(std::str::from_utf8(slice::from_raw_parts(
+        fn_name as *const u8,
+        fn_name_len
+    ))
+    .map_err(|_| String::from("invalid argument 'fn_name': invalid UTF-8 encoded")));
+    let has_fn_info = try_deref_mut!(has_fn_info);
+    let fn_info = try_deref_mut!(fn_info);
+    match runtime.get_function_definition(name) {
         Some(info) => {
             *has_fn_info = true;
-            fn_info.0 = Arc::into_raw(info) as *const c_void;
+            *fn_info = info.into()
         }
         None => *has_fn_info = false,
     }
@@ -267,39 +248,23 @@ pub unsafe extern "C" fn mun_runtime_get_function_info(
 /// an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
 #[no_mangle]
 pub unsafe extern "C" fn mun_runtime_get_type_info_by_name(
-    runtime: RuntimeHandle,
+    runtime: Runtime,
     type_name: *const c_char,
     has_type_info: *mut bool,
-    type_info: *mut TypeInfoHandle,
+    type_info: *mut Type,
 ) -> ErrorHandle {
-    let runtime = match (runtime.0 as *mut Runtime).as_ref() {
-        Some(runtime) => runtime,
-        None => return ErrorHandle::new("Invalid argument: 'runtime' is null pointer."),
-    };
-
-    if type_name.is_null() {
-        return ErrorHandle::new("Invalid argument: 'type_name' is null pointer.");
-    }
-
-    let type_name = match CStr::from_ptr(type_name).to_str() {
-        Ok(name) => name,
-        Err(_) => return ErrorHandle::new("Invalid argument: 'type_name' is not UTF-8 encoded."),
-    };
-
-    let has_type_info = match has_type_info.as_mut() {
-        Some(has_type) => has_type,
-        None => return ErrorHandle::new("Invalid argument: 'has_type_info' is null pointer."),
-    };
-
-    let type_info = match type_info.as_mut() {
-        Some(type_info) => type_info,
-        None => return ErrorHandle::new("Invalid argument: 'type_info' is null pointer."),
-    };
-
+    let runtime = mun_error_try!(runtime
+        .inner()
+        .map_err(|e| format!("invalid argument 'runtime': {e}")));
+    let type_name =
+        mun_error_try!(try_convert_c_string(type_name)
+            .map_err(|e| format!("invalid argument 'type_name': {e}")));
+    let has_type_info = try_deref_mut!(has_type_info);
+    let type_info = try_deref_mut!(type_info);
     match runtime.get_type_info_by_name(type_name) {
         Some(info) => {
             *has_type_info = true;
-            type_info.0 = Arc::into_raw(info) as *const c_void;
+            *type_info = info.into();
         }
         None => *has_type_info = false,
     }
@@ -320,37 +285,22 @@ pub unsafe extern "C" fn mun_runtime_get_type_info_by_name(
 /// an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
 #[no_mangle]
 pub unsafe extern "C" fn mun_runtime_get_type_info_by_id(
-    runtime: RuntimeHandle,
+    runtime: Runtime,
     type_id: *const abi::TypeId,
     has_type_info: *mut bool,
-    type_info: *mut TypeInfoHandle,
+    type_info: *mut Type,
 ) -> ErrorHandle {
-    let runtime = match (runtime.0 as *mut Runtime).as_ref() {
-        Some(runtime) => runtime,
-        None => return ErrorHandle::new("Invalid argument: 'runtime' is null pointer."),
-    };
-
-    let type_id = match type_id.as_ref() {
-        Some(type_id) => type_id,
-        None => {
-            return ErrorHandle::new("Invalid argument: 'type_id' is null pointer.");
-        }
-    };
-
-    let has_type_info = match has_type_info.as_mut() {
-        Some(has_type) => has_type,
-        None => return ErrorHandle::new("Invalid argument: 'has_type_info' is null pointer."),
-    };
-
-    let type_info = match type_info.as_mut() {
-        Some(type_info) => type_info,
-        None => return ErrorHandle::new("Invalid argument: 'type_info' is null pointer."),
-    };
+    let runtime = mun_error_try!(runtime
+        .inner()
+        .map_err(|e| format!("invalid argument 'runtime': {e}")));
+    let type_id = try_deref!(type_id);
+    let has_type_info = try_deref_mut!(has_type_info);
+    let type_info = try_deref_mut!(type_info);
 
     match runtime.get_type_info_by_id(type_id) {
         Some(info) => {
             *has_type_info = true;
-            type_info.0 = Arc::into_raw(info) as *const c_void;
+            *type_info = info.into();
         }
         None => *has_type_info = false,
     }
@@ -369,20 +319,11 @@ pub unsafe extern "C" fn mun_runtime_get_type_info_by_id(
 /// This function receives raw pointers as parameters. If any of the arguments is a null pointer,
 /// an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
 #[no_mangle]
-pub unsafe extern "C" fn mun_runtime_update(
-    handle: RuntimeHandle,
-    updated: *mut bool,
-) -> ErrorHandle {
-    let runtime = match (handle.0 as *mut Runtime).as_mut() {
-        Some(runtime) => runtime,
-        None => return ErrorHandle::new("Invalid argument: 'runtime' is null pointer."),
-    };
-
-    let updated = match updated.as_mut() {
-        Some(updated) => updated,
-        None => return ErrorHandle::new("Invalid argument: 'updated' is null pointer."),
-    };
-
+pub unsafe extern "C" fn mun_runtime_update(runtime: Runtime, updated: *mut bool) -> ErrorHandle {
+    let runtime = mun_error_try!(runtime
+        .inner_mut()
+        .map_err(|e| format!("invalid argument 'runtime': {e}")));
+    let updated = try_deref_mut!(updated);
     *updated = runtime.update();
     ErrorHandle::default()
 }
@@ -390,12 +331,14 @@ pub unsafe extern "C" fn mun_runtime_update(
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{error::mun_error_destroy, test_invalid_runtime, test_util::TestDriver};
-    use memory::HasStaticTypeInfo;
+    use crate::{test_invalid_runtime, test_util::TestDriver};
+    use capi_utils::error::mun_error_destroy;
+    use capi_utils::{assert_error_snapshot, assert_getter1, assert_getter2, assert_getter3};
+    use memory::HasStaticType;
     use std::{ffi::CString, mem::MaybeUninit, ptr};
 
     test_invalid_runtime!(
-        runtime_get_function_info(ptr::null(), ptr::null_mut(), ptr::null_mut()),
+        runtime_find_function_definition(ptr::null(), 0, ptr::null_mut(), ptr::null_mut()),
         runtime_get_type_info_by_name(ptr::null(), ptr::null_mut(), ptr::null_mut()),
         runtime_get_type_info_by_id(ptr::null(), ptr::null_mut(), ptr::null_mut()),
         runtime_update(ptr::null_mut())
@@ -403,39 +346,26 @@ mod tests {
 
     #[test]
     fn test_runtime_create_invalid_lib_path() {
-        let handle =
-            unsafe { mun_runtime_create(ptr::null(), RuntimeOptions::default(), ptr::null_mut()) };
-        assert_ne!(handle.0, ptr::null());
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'library_path' is null pointer."
+        assert_error_snapshot!(
+            unsafe { mun_runtime_create(ptr::null(), RuntimeOptions::default(), ptr::null_mut()) },
+            @r###""invalid argument \'library_path\': null pointer""###
         );
-
-        unsafe { mun_error_destroy(handle) };
     }
 
     #[test]
     fn test_runtime_create_invalid_lib_path_encoding() {
         let invalid_encoding = ['�', '\0'];
 
-        let handle = unsafe {
-            mun_runtime_create(
-                invalid_encoding.as_ptr() as *const _,
-                RuntimeOptions::default(),
-                ptr::null_mut(),
-            )
-        };
-        assert_ne!(handle.0, ptr::null());
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'library_path' is not UTF-8 encoded."
+        assert_error_snapshot!(
+            unsafe {
+                mun_runtime_create(
+                    invalid_encoding.as_ptr() as *const _,
+                    RuntimeOptions::default(),
+                    ptr::null_mut(),
+                )
+            },
+            @r###""invalid argument \'library_path\': invalid UTF-8 encoded""###
         );
-
-        unsafe { mun_error_destroy(handle) };
     }
 
     #[test]
@@ -447,38 +377,23 @@ mod tests {
             ..Default::default()
         };
 
-        let handle = unsafe { mun_runtime_create(lib_path.into_raw(), options, ptr::null_mut()) };
-        assert_ne!(handle.0, ptr::null());
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'functions' is null pointer."
+        let mut handle = MaybeUninit::uninit();
+        assert_error_snapshot!(
+            unsafe { mun_runtime_create(lib_path.into_raw(), options, handle.as_mut_ptr()) },
+            @r###""invalid argument: \'functions\' is null pointer.""###
         );
-
-        unsafe { mun_error_destroy(handle) };
     }
 
     #[test]
     fn test_runtime_create_invalid_handle() {
         let lib_path = CString::new("some/path").expect("Invalid library path");
 
-        let handle = unsafe {
-            mun_runtime_create(
-                lib_path.into_raw(),
-                RuntimeOptions::default(),
-                ptr::null_mut(),
-            )
-        };
-        assert_ne!(handle.0, ptr::null());
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'handle' is null pointer."
+        assert_error_snapshot!(
+            unsafe {
+                mun_runtime_create(lib_path.into_raw(), RuntimeOptions::default(), ptr::null_mut())
+            },
+            @r###""invalid argument \'handle\': null pointer""###
         );
-
-        unsafe { mun_error_destroy(handle) };
     }
 
     #[test]
@@ -500,18 +415,11 @@ mod tests {
             ..Default::default()
         };
 
-        let mut runtime = MaybeUninit::uninit();
-        let handle =
-            unsafe { mun_runtime_create(lib_path.into_raw(), options, runtime.as_mut_ptr()) };
-        assert_ne!(handle.0, ptr::null());
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            format!("Invalid function name: function name null pointer")
+        let mut handle = MaybeUninit::uninit();
+        assert_error_snapshot!(
+            unsafe { mun_runtime_create(lib_path.into_raw(), options, handle.as_mut_ptr()) },
+            @r###""invalid function name: null pointer""###
         );
-
-        unsafe { mun_error_destroy(handle) };
     }
 
     #[test]
@@ -534,18 +442,11 @@ mod tests {
             ..Default::default()
         };
 
-        let mut runtime = MaybeUninit::uninit();
-        let handle =
-            unsafe { mun_runtime_create(lib_path.into_raw(), options, runtime.as_mut_ptr()) };
-        assert_ne!(handle.0, ptr::null());
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            format!("Invalid function name: function name is not UTF-8 encoded: invalid utf-8 sequence of 1 bytes from index 0")
+        let mut handle = MaybeUninit::uninit();
+        assert_error_snapshot!(
+            unsafe { mun_runtime_create(lib_path.into_raw(), options, handle.as_mut_ptr()) },
+            @r###""invalid function name: invalid UTF-8 encoded""###
         );
-
-        unsafe { mun_error_destroy(handle) };
     }
 
     #[test]
@@ -556,7 +457,7 @@ mod tests {
         let functions = vec![ExternalFunctionDefinition {
             name: function_name.as_ptr(),
             arg_types: ptr::null(),
-            return_type: TypeInfoHandle::null(),
+            return_type: Type::null(),
             num_args: 0,
             fn_ptr: ptr::null(),
         }];
@@ -567,18 +468,11 @@ mod tests {
             ..Default::default()
         };
 
-        let mut runtime = MaybeUninit::uninit();
-        let handle =
-            unsafe { mun_runtime_create(lib_path.into_raw(), options, runtime.as_mut_ptr()) };
-        assert_ne!(handle.0, ptr::null());
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            format!("Invalid function 'foobar': 'return_type' is null pointer.")
+        let mut handle = MaybeUninit::uninit();
+        assert_error_snapshot!(
+            unsafe { mun_runtime_create(lib_path.into_raw(), options, handle.as_mut_ptr()) },
+            @r###""invalid function \'foobar\': \'return_type\': null pointer""###
         );
-
-        unsafe { mun_error_destroy(handle) };
     }
 
     #[test]
@@ -601,25 +495,18 @@ mod tests {
             ..Default::default()
         };
 
-        let mut runtime = MaybeUninit::uninit();
-        let handle =
-            unsafe { mun_runtime_create(lib_path.into_raw(), options, runtime.as_mut_ptr()) };
-        assert_ne!(handle.0, ptr::null());
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            format!("Invalid function 'foobar': 'arg_types' is null pointer.")
+        let mut handle = MaybeUninit::uninit();
+        assert_error_snapshot!(
+            unsafe { mun_runtime_create(lib_path.into_raw(), options, handle.as_mut_ptr()) },
+            @r###""invalid function \'foobar\': \'arg_types\' is null pointer.""###
         );
-
-        unsafe { mun_error_destroy(handle) };
     }
 
     #[test]
     fn test_runtime_create_invalid_user_function_arg_types() {
         let lib_path = CString::new("some/path").expect("Invalid library path");
         let function_name = CString::new("foobar").unwrap();
-        let arg_types = [TypeInfoHandle::null()];
+        let arg_types = [Type::null()];
 
         let type_id = <()>::type_info().clone().into();
         let functions = vec![ExternalFunctionDefinition {
@@ -636,18 +523,11 @@ mod tests {
             ..Default::default()
         };
 
-        let mut runtime = MaybeUninit::uninit();
-        let handle =
-            unsafe { mun_runtime_create(lib_path.into_raw(), options, runtime.as_mut_ptr()) };
-        assert_ne!(handle.0, ptr::null());
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            format!("Invalid function 'foobar': argument #1 is null pointer.")
+        let mut handle = MaybeUninit::uninit();
+        assert_error_snapshot!(
+            unsafe { mun_runtime_create(lib_path.into_raw(), options, handle.as_mut_ptr()) },
+            @r###""invalid function \'foobar\': argument #1: null pointer""###
         );
-
-        unsafe { mun_error_destroy(handle) };
     }
 
     #[test]
@@ -658,22 +538,18 @@ mod tests {
     "#,
         );
 
-        let handle = unsafe {
-            mun_runtime_get_function_info(
-                driver.runtime,
-                ptr::null(),
-                ptr::null_mut(),
-                ptr::null_mut(),
-            )
-        };
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'fn_name' is null pointer."
+        assert_error_snapshot!(
+            unsafe {
+                mun_runtime_find_function_definition(
+                    driver.runtime,
+                    ptr::null(),
+                    0,
+                    ptr::null_mut(),
+                    ptr::null_mut(),
+                )
+            },
+            @r###""invalid argument \'fn_name\': null pointer""###
         );
-
-        unsafe { mun_error_destroy(handle) };
     }
 
     #[test]
@@ -685,22 +561,18 @@ mod tests {
         );
 
         let invalid_encoding = ['�', '\0'];
-        let handle = unsafe {
-            mun_runtime_get_function_info(
-                driver.runtime,
-                invalid_encoding.as_ptr() as *const _,
-                ptr::null_mut(),
-                ptr::null_mut(),
-            )
-        };
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'fn_name' is not UTF-8 encoded."
+        assert_error_snapshot!(
+            unsafe {
+                mun_runtime_find_function_definition(
+                    driver.runtime,
+                    invalid_encoding.as_ptr() as *const _,
+                    3,
+                    ptr::null_mut(),
+                    ptr::null_mut(),
+                )
+            },
+            @r###""invalid argument \'fn_name\': invalid UTF-8 encoded""###
         );
-
-        unsafe { mun_error_destroy(handle) };
     }
 
     #[test]
@@ -712,22 +584,18 @@ mod tests {
         );
 
         let fn_name = CString::new("main").expect("Invalid function name");
-        let handle = unsafe {
-            mun_runtime_get_function_info(
-                driver.runtime,
-                fn_name.as_ptr(),
-                ptr::null_mut(),
-                ptr::null_mut(),
-            )
-        };
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'has_fn_info' is null pointer."
+        assert_error_snapshot!(
+            unsafe {
+                mun_runtime_find_function_definition(
+                    driver.runtime,
+                    fn_name.as_ptr(),
+                    fn_name.as_bytes().len(),
+                    ptr::null_mut(),
+                    ptr::null_mut(),
+                )
+            },
+            @r###""invalid argument \'has_fn_info\': null pointer""###
         );
-
-        unsafe { mun_error_destroy(handle) };
     }
 
     #[test]
@@ -739,23 +607,19 @@ mod tests {
         );
 
         let fn_name = CString::new("main").expect("Invalid function name");
-        let mut has_fn_info = false;
-        let handle = unsafe {
-            mun_runtime_get_function_info(
-                driver.runtime,
-                fn_name.as_ptr(),
-                &mut has_fn_info as *mut _,
-                ptr::null_mut(),
-            )
-        };
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'fn_info' is null pointer."
+        let mut has_fn_info = MaybeUninit::uninit();
+        assert_error_snapshot!(
+            unsafe {
+                mun_runtime_find_function_definition(
+                    driver.runtime,
+                    fn_name.as_ptr(),
+                    fn_name.as_bytes().len(),
+                    has_fn_info.as_mut_ptr(),
+                    ptr::null_mut(),
+                )
+            },
+            @r###""invalid argument \'fn_info\': null pointer""###
         );
-
-        unsafe { mun_error_destroy(handle) };
     }
 
     #[test]
@@ -767,17 +631,13 @@ mod tests {
         );
 
         let fn_name = CString::new("add").expect("Invalid function name");
-        let mut has_fn_info = false;
-        let mut fn_definition = MaybeUninit::uninit();
-        let handle = unsafe {
-            mun_runtime_get_function_info(
-                driver.runtime,
-                fn_name.as_ptr(),
-                &mut has_fn_info as *mut _,
-                fn_definition.as_mut_ptr(),
-            )
-        };
-        assert_eq!(handle.0, ptr::null());
+        assert_getter3!(mun_runtime_find_function_definition(
+            driver.runtime,
+            fn_name.as_ptr(),
+            fn_name.as_bytes().len(),
+            has_fn_info,
+            _fn_definition,
+        ));
         assert!(!has_fn_info);
     }
 
@@ -790,19 +650,14 @@ mod tests {
         );
 
         let fn_name = CString::new("main").expect("Invalid function name");
-        let mut has_fn_info = false;
-        let mut fn_definition = MaybeUninit::uninit();
-        let handle = unsafe {
-            mun_runtime_get_function_info(
-                driver.runtime,
-                fn_name.as_ptr(),
-                &mut has_fn_info as *mut _,
-                fn_definition.as_mut_ptr(),
-            )
-        };
-        assert_eq!(handle.0, ptr::null());
+        assert_getter3!(mun_runtime_find_function_definition(
+            driver.runtime,
+            fn_name.as_ptr(),
+            fn_name.as_bytes().len(),
+            has_fn_info,
+            _fn_definition,
+        ));
         assert!(has_fn_info);
-        let _fn_definition = unsafe { fn_definition.assume_init() };
     }
 
     #[test]
@@ -813,22 +668,17 @@ mod tests {
     "#,
         );
 
-        let handle = unsafe {
-            mun_runtime_get_type_info_by_name(
-                driver.runtime,
-                ptr::null(),
-                ptr::null_mut(),
-                ptr::null_mut(),
-            )
-        };
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'type_name' is null pointer."
+        assert_error_snapshot!(
+            unsafe {
+                mun_runtime_get_type_info_by_name(
+                    driver.runtime,
+                    ptr::null(),
+                    ptr::null_mut(),
+                    ptr::null_mut(),
+                )
+            },
+            @r###""invalid argument \'type_name\': null pointer""###
         );
-
-        unsafe { mun_error_destroy(handle) };
     }
 
     #[test]
@@ -840,22 +690,17 @@ mod tests {
         );
 
         let invalid_encoding = ['�', '\0'];
-        let handle = unsafe {
-            mun_runtime_get_type_info_by_name(
-                driver.runtime,
-                invalid_encoding.as_ptr() as *const _,
-                ptr::null_mut(),
-                ptr::null_mut(),
-            )
-        };
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'type_name' is not UTF-8 encoded."
+        assert_error_snapshot!(
+            unsafe {
+                mun_runtime_get_type_info_by_name(
+                    driver.runtime,
+                    invalid_encoding.as_ptr() as *const _,
+                    ptr::null_mut(),
+                    ptr::null_mut(),
+                )
+            },
+            @r###""invalid argument \'type_name\': invalid UTF-8 encoded""###
         );
-
-        unsafe { mun_error_destroy(handle) };
     }
 
     #[test]
@@ -867,22 +712,17 @@ mod tests {
         );
 
         let type_name = CString::new("Foo").expect("Invalid type name");
-        let handle = unsafe {
-            mun_runtime_get_type_info_by_name(
-                driver.runtime,
-                type_name.as_ptr(),
-                ptr::null_mut(),
-                ptr::null_mut(),
-            )
-        };
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'has_type_info' is null pointer."
+        assert_error_snapshot!(
+            unsafe {
+                mun_runtime_get_type_info_by_name(
+                    driver.runtime,
+                    type_name.as_ptr(),
+                    ptr::null_mut(),
+                    ptr::null_mut(),
+                )
+            },
+            @r###""invalid argument \'has_type_info\': null pointer""###
         );
-
-        unsafe { mun_error_destroy(handle) };
     }
 
     #[test]
@@ -895,22 +735,17 @@ mod tests {
 
         let type_name = CString::new("Foo").expect("Invalid type name");
         let mut has_type_info = false;
-        let handle = unsafe {
-            mun_runtime_get_type_info_by_name(
-                driver.runtime,
-                type_name.as_ptr(),
-                &mut has_type_info as *mut _,
-                ptr::null_mut(),
-            )
-        };
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'type_info' is null pointer."
+        assert_error_snapshot!(
+            unsafe {
+                mun_runtime_get_type_info_by_name(
+                    driver.runtime,
+                    type_name.as_ptr(),
+                    &mut has_type_info as *mut _,
+                    ptr::null_mut(),
+                )
+            },
+            @r###""invalid argument \'type_info\': null pointer""###
         );
-
-        unsafe { mun_error_destroy(handle) };
     }
 
     #[test]
@@ -922,17 +757,12 @@ mod tests {
         );
 
         let type_name = CString::new("Bar").expect("Invalid type name");
-        let mut has_type_info = false;
-        let mut type_info = MaybeUninit::uninit();
-        let handle = unsafe {
-            mun_runtime_get_type_info_by_name(
-                driver.runtime,
-                type_name.as_ptr(),
-                &mut has_type_info as *mut _,
-                type_info.as_mut_ptr(),
-            )
-        };
-        assert_eq!(handle.0, ptr::null());
+        assert_getter2!(mun_runtime_get_type_info_by_name(
+            driver.runtime,
+            type_name.as_ptr(),
+            has_type_info,
+            _type_info,
+        ));
         assert!(!has_type_info);
     }
 
@@ -945,19 +775,13 @@ mod tests {
         );
 
         let type_name = CString::new("Foo").expect("Invalid type name");
-        let mut has_type_info = false;
-        let mut type_info = MaybeUninit::uninit();
-        let handle = unsafe {
-            mun_runtime_get_type_info_by_name(
-                driver.runtime,
-                type_name.as_ptr(),
-                &mut has_type_info as *mut _,
-                type_info.as_mut_ptr(),
-            )
-        };
-        assert_eq!(handle.0, ptr::null());
+        assert_getter2!(mun_runtime_get_type_info_by_name(
+            driver.runtime,
+            type_name.as_ptr(),
+            has_type_info,
+            _type_info,
+        ));
         assert!(has_type_info);
-        let _type_info = unsafe { type_info.assume_init() };
     }
 
     #[test]
@@ -968,22 +792,17 @@ mod tests {
     "#,
         );
 
-        let handle = unsafe {
-            mun_runtime_get_type_info_by_id(
-                driver.runtime,
-                ptr::null(),
-                ptr::null_mut(),
-                ptr::null_mut(),
-            )
-        };
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'type_id' is null pointer."
+        assert_error_snapshot!(
+            unsafe {
+                mun_runtime_get_type_info_by_id(
+                    driver.runtime,
+                    ptr::null(),
+                    ptr::null_mut(),
+                    ptr::null_mut(),
+                )
+            },
+            @r###""invalid argument \'type_id\': null pointer""###
         );
-
-        unsafe { mun_error_destroy(handle) };
     }
 
     #[test]
@@ -995,22 +814,17 @@ mod tests {
         );
 
         let type_id = abi::TypeId::Concrete(abi::Guid([0; 16]));
-        let handle = unsafe {
-            mun_runtime_get_type_info_by_id(
-                driver.runtime,
-                &type_id as *const abi::TypeId,
-                ptr::null_mut(),
-                ptr::null_mut(),
-            )
-        };
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'has_type_info' is null pointer."
+        assert_error_snapshot!(
+            unsafe {
+                mun_runtime_get_type_info_by_id(
+                    driver.runtime,
+                    &type_id as *const abi::TypeId,
+                    ptr::null_mut(),
+                    ptr::null_mut(),
+                )
+            },
+            @r###""invalid argument \'has_type_info\': null pointer""###
         );
-
-        unsafe { mun_error_destroy(handle) };
     }
 
     #[test]
@@ -1023,22 +837,17 @@ mod tests {
 
         let type_id = abi::TypeId::Concrete(abi::Guid([0; 16]));
         let mut has_type_info = false;
-        let handle = unsafe {
-            mun_runtime_get_type_info_by_id(
-                driver.runtime,
-                &type_id as *const abi::TypeId,
-                &mut has_type_info as *mut _,
-                ptr::null_mut(),
-            )
-        };
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'type_info' is null pointer."
+        assert_error_snapshot!(
+            unsafe {
+                mun_runtime_get_type_info_by_id(
+                    driver.runtime,
+                    &type_id as *const abi::TypeId,
+                    &mut has_type_info as *mut _,
+                    ptr::null_mut(),
+                )
+            },
+            @r###""invalid argument \'type_info\': null pointer""###
         );
-
-        unsafe { mun_error_destroy(handle) };
     }
 
     #[test]
@@ -1050,17 +859,12 @@ mod tests {
         );
 
         let type_id = abi::TypeId::Concrete(abi::Guid([0; 16]));
-        let mut has_type_info = false;
-        let mut type_info = MaybeUninit::uninit();
-        let handle = unsafe {
-            mun_runtime_get_type_info_by_id(
-                driver.runtime,
-                &type_id as *const abi::TypeId,
-                &mut has_type_info as *mut _,
-                type_info.as_mut_ptr(),
-            )
-        };
-        assert_eq!(handle.0, ptr::null());
+        assert_getter2!(mun_runtime_get_type_info_by_id(
+            driver.runtime,
+            &type_id as *const abi::TypeId,
+            has_type_info,
+            _type_info,
+        ));
         assert!(!has_type_info);
     }
 
@@ -1073,17 +877,12 @@ mod tests {
         );
 
         let type_name = CString::new("Foo").expect("Invalid type name");
-        let mut has_type_info = false;
-        let mut type_info = MaybeUninit::uninit();
-        let handle = unsafe {
-            mun_runtime_get_type_info_by_name(
-                driver.runtime,
-                type_name.as_ptr(),
-                &mut has_type_info as *mut _,
-                type_info.as_mut_ptr(),
-            )
-        };
-        assert_eq!(handle.0, ptr::null());
+        assert_getter2!(mun_runtime_get_type_info_by_name(
+            driver.runtime,
+            type_name.as_ptr(),
+            has_type_info,
+            _type_info,
+        ));
         assert!(has_type_info);
     }
 
@@ -1095,15 +894,10 @@ mod tests {
     "#,
         );
 
-        let handle = unsafe { mun_runtime_update(driver.runtime, ptr::null_mut()) };
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'updated' is null pointer."
+        assert_error_snapshot!(
+            unsafe { mun_runtime_update(driver.runtime, ptr::null_mut()) },
+            @r###""invalid argument \'updated\': null pointer""###
         );
-
-        unsafe { mun_error_destroy(handle) };
     }
 
     #[test]
@@ -1114,8 +908,6 @@ mod tests {
     "#,
         );
 
-        let mut updated = false;
-        let handle = unsafe { mun_runtime_update(driver.runtime, &mut updated as *mut _) };
-        assert_eq!(handle.0, ptr::null());
+        assert_getter1!(mun_runtime_update(driver.runtime, _updated));
     }
 }
diff --git a/crates/mun_runtime_capi/src/struct_info.rs b/crates/mun_runtime_capi/src/struct_info.rs
deleted file mode 100644
index 5879ef693..000000000
--- a/crates/mun_runtime_capi/src/struct_info.rs
+++ /dev/null
@@ -1,274 +0,0 @@
-//! Exposes struct information using the C ABI.
-
-use crate::{
-    error::ErrorHandle,
-    field_info::{FieldInfoHandle, FieldInfoSpan},
-};
-use memory::{FieldInfo, StructInfo};
-use std::{ffi::c_void, mem, ptr};
-
-/// A C-style handle to a `StructInfo`.
-#[repr(C)]
-#[derive(Clone, Copy, Debug)]
-pub struct StructInfoHandle(pub *const c_void);
-
-impl StructInfoHandle {
-    /// A null handle.
-    pub fn null() -> Self {
-        Self(ptr::null())
-    }
-}
-
-/// Retrieves information about the struct's fields.
-///
-/// # Safety
-///
-/// The caller is responsible for calling `mun_field_info_span_destroy` on the returned span.
-///
-/// This function might result in undefined behavior if the [`crate::TypeInfoHandle`] associated
-/// with this `StructInfoHandle` has been deallocated.
-#[no_mangle]
-pub unsafe extern "C" fn mun_struct_info_fields(
-    struct_info: StructInfoHandle,
-    field_info_span: *mut FieldInfoSpan,
-) -> ErrorHandle {
-    let struct_info = match (struct_info.0 as *const StructInfo).as_ref() {
-        Some(struct_info) => struct_info,
-        None => return ErrorHandle::new("Invalid argument: 'struct_info' is null pointer."),
-    };
-
-    let field_info_span = match field_info_span.as_mut() {
-        Some(field_info_span) => field_info_span,
-        None => return ErrorHandle::new("Invalid argument: 'field_info_span' is null pointer."),
-    };
-
-    let mut fields: Vec<FieldInfoHandle> = struct_info
-        .fields
-        .iter()
-        .map(|field| FieldInfoHandle(field as *const FieldInfo as *const c_void))
-        .collect();
-
-    field_info_span.len = fields.len();
-    field_info_span.data = if fields.is_empty() {
-        ptr::null()
-    } else {
-        fields.shrink_to_fit();
-        fields.as_ptr() as *const _
-    };
-
-    // Ownership is transferred
-    mem::forget(fields);
-
-    ErrorHandle::default()
-}
-
-/// Retrieves the struct's memory kind.
-///
-/// # Safety
-///
-/// This function might result in undefined behavior if the [`crate::TypeInfoHandle`] associated
-/// with this `StructInfoHandle` has been deallocated.
-#[no_mangle]
-pub unsafe extern "C" fn mun_struct_info_memory_kind(
-    struct_info: StructInfoHandle,
-    memory_kind: *mut abi::StructMemoryKind,
-) -> ErrorHandle {
-    let struct_info = match (struct_info.0 as *const StructInfo).as_ref() {
-        Some(struct_info) => struct_info,
-        None => return ErrorHandle::new("Invalid argument: 'struct_info' is null pointer."),
-    };
-
-    let memory_kind = match memory_kind.as_mut() {
-        Some(memory_kind) => memory_kind,
-        None => return ErrorHandle::new("Invalid argument: 'memory_kind' is null pointer."),
-    };
-
-    *memory_kind = struct_info.memory_kind;
-
-    ErrorHandle::default()
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::{
-        error::mun_error_destroy,
-        field_info::{mun_field_info_name, mun_field_info_span_destroy},
-        runtime::RuntimeHandle,
-        test_util::TestDriver,
-        type_info::{mun_type_info_data, tests::get_type_info_by_name},
-    };
-    use std::{ffi::CStr, mem::MaybeUninit, ptr, slice};
-
-    fn get_struct_info_by_name<T: Into<Vec<u8>>>(
-        runtime: RuntimeHandle,
-        type_name: T,
-    ) -> StructInfoHandle {
-        let type_info = get_type_info_by_name(runtime, type_name);
-
-        let mut type_info_data = MaybeUninit::uninit();
-        let handle = unsafe { mun_type_info_data(type_info, type_info_data.as_mut_ptr()) };
-        assert_eq!(handle.0, ptr::null());
-
-        let type_info_data = unsafe { type_info_data.assume_init() };
-        type_info_data
-            .as_struct()
-            .expect("the type name does not represent a struct")
-    }
-
-    #[test]
-    fn test_struct_info_fields_invalid_struct_info() {
-        let handle = unsafe { mun_struct_info_fields(StructInfoHandle::null(), ptr::null_mut()) };
-        assert_ne!(handle.0, ptr::null());
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'struct_info' is null pointer."
-        );
-
-        unsafe { mun_error_destroy(handle) };
-    }
-
-    #[test]
-    fn test_struct_info_fields_invalid_arg() {
-        let driver = TestDriver::new(
-            r#"
-            pub struct Foo;"#,
-        );
-
-        let foo_struct_info = get_struct_info_by_name(driver.runtime, "Foo");
-        let handle = unsafe { mun_struct_info_fields(foo_struct_info, ptr::null_mut()) };
-        assert_ne!(handle.0, ptr::null());
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'field_info_span' is null pointer."
-        );
-
-        unsafe { mun_error_destroy(handle) };
-    }
-
-    #[test]
-    fn test_struct_info_fields_none() {
-        let driver = TestDriver::new(
-            r#"
-            pub struct Foo;"#,
-        );
-
-        let foo_struct_info = get_struct_info_by_name(driver.runtime, "Foo");
-
-        let mut foo_fields_span = MaybeUninit::uninit();
-        let handle =
-            unsafe { mun_struct_info_fields(foo_struct_info, foo_fields_span.as_mut_ptr()) };
-        assert_eq!(handle.0, ptr::null());
-
-        let foo_fields_span = unsafe { foo_fields_span.assume_init() };
-        assert_eq!(foo_fields_span.data, ptr::null());
-        assert_eq!(foo_fields_span.len, 0);
-    }
-
-    #[test]
-    fn test_struct_info_fields_some() {
-        let driver = TestDriver::new(
-            r#"
-            pub struct Foo {
-                bar: i8,
-                baz: f32,
-            }"#,
-        );
-
-        let foo_struct_info = get_struct_info_by_name(driver.runtime, "Foo");
-
-        let mut foo_fields_span = MaybeUninit::uninit();
-        let handle =
-            unsafe { mun_struct_info_fields(foo_struct_info, foo_fields_span.as_mut_ptr()) };
-        assert_eq!(handle.0, ptr::null());
-
-        let foo_fields_span = unsafe { foo_fields_span.assume_init() };
-        assert_eq!(foo_fields_span.len, 2);
-
-        let foo_fields: Vec<_> =
-            unsafe { slice::from_raw_parts(foo_fields_span.data, foo_fields_span.len) }
-                .iter()
-                .map(|field_info| {
-                    unsafe { CStr::from_ptr(mun_field_info_name(*field_info)) }
-                        .to_str()
-                        .expect("invalid field name")
-                        .to_owned()
-                })
-                .collect();
-
-        assert!(unsafe { mun_field_info_span_destroy(foo_fields_span) });
-
-        assert_eq!(vec![String::from("bar"), String::from("baz")], foo_fields);
-    }
-
-    #[test]
-    fn test_struct_info_memory_kind_invalid_struct_info() {
-        let handle =
-            unsafe { mun_struct_info_memory_kind(StructInfoHandle::null(), ptr::null_mut()) };
-        assert_ne!(handle.0, ptr::null());
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'struct_info' is null pointer."
-        );
-
-        unsafe { mun_error_destroy(handle) };
-    }
-
-    #[test]
-    fn test_struct_info_memory_kind_invalid_memory_kind() {
-        let driver = TestDriver::new(
-            r#"
-            pub struct Foo;"#,
-        );
-
-        let struct_info = get_struct_info_by_name(driver.runtime, "Foo");
-        let handle = unsafe { mun_struct_info_memory_kind(struct_info, ptr::null_mut()) };
-        assert_ne!(handle.0, ptr::null());
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'memory_kind' is null pointer."
-        );
-
-        unsafe { mun_error_destroy(handle) };
-    }
-
-    #[test]
-    fn test_struct_info_memory_kind() {
-        let driver = TestDriver::new(
-            r#"
-            pub struct Foo {
-                bar: i8,
-                baz: f32,
-            }
-
-            pub struct(value) Bar;"#,
-        );
-
-        let foo_struct_info = get_struct_info_by_name(driver.runtime, "Foo");
-        let bar_struct_info = get_struct_info_by_name(driver.runtime, "Bar");
-
-        let mut foo_memory_kind = MaybeUninit::uninit();
-        let handle =
-            unsafe { mun_struct_info_memory_kind(foo_struct_info, foo_memory_kind.as_mut_ptr()) };
-        assert_eq!(handle.0, ptr::null());
-
-        let mut bar_memory_kind = MaybeUninit::uninit();
-        let handle =
-            unsafe { mun_struct_info_memory_kind(bar_struct_info, bar_memory_kind.as_mut_ptr()) };
-        assert_eq!(handle.0, ptr::null());
-
-        let foo_memory_kind = unsafe { foo_memory_kind.assume_init() };
-        let bar_memory_kind = unsafe { bar_memory_kind.assume_init() };
-
-        assert_eq!(foo_memory_kind, abi::StructMemoryKind::Gc);
-        assert_eq!(bar_memory_kind, abi::StructMemoryKind::Value);
-    }
-}
diff --git a/crates/mun_runtime_capi/src/type_info.rs b/crates/mun_runtime_capi/src/type_info.rs
deleted file mode 100644
index 6cbb9a475..000000000
--- a/crates/mun_runtime_capi/src/type_info.rs
+++ /dev/null
@@ -1,740 +0,0 @@
-//! Exposes type information using the C ABI.
-
-use crate::{error::ErrorHandle, struct_info::StructInfoHandle};
-use memory::{HasStaticTypeInfo, StructInfo, TypeInfo};
-use std::mem::ManuallyDrop;
-use std::ops::Deref;
-use std::{
-    ffi::{c_void, CString},
-    os::raw::c_char,
-    ptr,
-    sync::Arc,
-};
-
-/// A C-style handle to a `TypeInfo`.
-#[repr(C)]
-#[derive(Clone, Copy)]
-pub struct TypeInfoHandle(pub *const c_void);
-
-impl TypeInfoHandle {
-    /// A null handle.
-    pub fn null() -> Self {
-        Self(ptr::null())
-    }
-}
-
-impl From<Arc<TypeInfo>> for TypeInfoHandle {
-    fn from(ty: Arc<TypeInfo>) -> Self {
-        TypeInfoHandle(Arc::into_raw(ty) as _)
-    }
-}
-
-/// A C-style handle to an array of `TypeInfoHandle`s.
-#[repr(C)]
-pub struct TypeInfoSpan {
-    /// Pointer to the start of the array buffer
-    pub data: *const TypeInfoHandle,
-    /// Length of the array (and capacity)
-    pub len: usize,
-}
-
-impl TypeInfoSpan {
-    /// Retrieves the `TypeInfoHandle` at the specified index, if within bounds.
-    /// Otherwise, returns null.
-    pub fn get(&self, index: usize) -> TypeInfoHandle {
-        if index < self.len {
-            // SAFETY: Bounds checking was performed
-            TypeInfoHandle(unsafe {
-                *(self.data as *const *const TypeInfo).add(index) as *const c_void
-            })
-        } else {
-            TypeInfoHandle::null()
-        }
-    }
-}
-
-/// Decrements the strong count of the `Arc<TypeInfo>` associated with `handle`.
-///
-/// # Safety
-///
-/// This function results in undefined behavior if the passed in `TypeInfoHandle` has already been
-/// deallocated in a previous call to [`mun_type_info_decrement_strong_count`].
-#[no_mangle]
-pub unsafe extern "C" fn mun_type_info_decrement_strong_count(handle: TypeInfoHandle) -> bool {
-    if !handle.0.is_null() {
-        Arc::decrement_strong_count(handle.0);
-        return true;
-    }
-
-    false
-}
-
-/// Increments the strong count of the `Arc<TypeInfo>` associated with `handle`.
-///
-/// # Safety
-///
-/// This function results in undefined behavior if the passed in `TypeInfoHandle` has already been
-/// deallocated in a previous call to [`mun_type_info_decrement_strong_count`].
-#[no_mangle]
-pub unsafe extern "C" fn mun_type_info_increment_strong_count(handle: TypeInfoHandle) -> bool {
-    if !handle.0.is_null() {
-        Arc::increment_strong_count(handle.0);
-        return true;
-    }
-
-    false
-}
-
-/// Retrieves the type's name.
-///
-/// # Safety
-///
-/// The caller is responsible for calling `mun_string_destroy` on the return pointer - if it is not null.
-///
-/// This function results in undefined behavior if the passed in `TypeInfoHandle` has been
-/// deallocated in a previous call to [`mun_type_info_decrement_strong_count`].
-#[no_mangle]
-pub unsafe extern "C" fn mun_type_info_name(type_info: TypeInfoHandle) -> *const c_char {
-    let type_info = match (type_info.0 as *const TypeInfo).as_ref() {
-        Some(type_info) => type_info,
-        None => return ptr::null(),
-    };
-
-    CString::new(type_info.name.clone()).unwrap().into_raw() as *const _
-}
-
-/// Returns true if the specified type info handles describe the same type.
-///
-/// # Safety
-///
-/// This function results in undefined behavior if any of the the passed in `TypeInfoHandle` have
-/// been deallocated in a previous call to [`mun_type_info_decrement_strong_count`].
-#[no_mangle]
-pub unsafe extern "C" fn mun_type_info_eq(a: TypeInfoHandle, b: TypeInfoHandle) -> bool {
-    let a = match (a.0 as *const TypeInfo).as_ref() {
-        None => return false,
-        Some(a) => a,
-    };
-
-    let b = match (b.0 as *const TypeInfo).as_ref() {
-        None => return false,
-        Some(b) => b,
-    };
-
-    a == b
-}
-
-/// Returns the TypeInfoHandle of a pointer to the given TypeInfoHandle.
-///
-/// # Safety
-///
-/// This function results in undefined behavior if any of the the passed in `TypeInfoHandle` have
-/// been deallocated in a previous call to [`mun_type_info_decrement_strong_count`].
-#[no_mangle]
-pub unsafe extern "C" fn mun_type_info_pointer_type(
-    handle: TypeInfoHandle,
-    mutable: bool,
-) -> TypeInfoHandle {
-    if handle.0.is_null() {
-        return TypeInfoHandle::null();
-    }
-
-    let type_info = ManuallyDrop::new(Arc::from_raw(handle.0 as *const TypeInfo));
-    type_info.deref().pointer_type(mutable).into()
-}
-
-/// Retrieves the type's size.
-///
-/// # Safety
-///
-/// This function results in undefined behavior if the passed in `TypeInfoHandle` has been
-/// deallocated in a previous call to [`mun_type_info_decrement_strong_count`].
-#[no_mangle]
-pub unsafe extern "C" fn mun_type_info_size(
-    type_info: TypeInfoHandle,
-    size: *mut usize,
-) -> ErrorHandle {
-    let type_info = match (type_info.0 as *const TypeInfo).as_ref() {
-        Some(type_info) => type_info,
-        None => return ErrorHandle::new("Invalid argument: 'type_info' is null pointer."),
-    };
-
-    let size = match size.as_mut() {
-        Some(size) => size,
-        None => return ErrorHandle::new("Invalid argument: 'size' is null pointer."),
-    };
-
-    *size = type_info.layout.size();
-
-    ErrorHandle::default()
-}
-
-/// Retrieves the type's alignment.
-///
-/// # Safety
-///
-/// This function results in undefined behavior if the passed in `TypeInfoHandle` has been
-/// deallocated in a previous call to [`mun_type_info_decrement_strong_count`].
-#[no_mangle]
-pub unsafe extern "C" fn mun_type_info_align(
-    type_info: TypeInfoHandle,
-    align: *mut usize,
-) -> ErrorHandle {
-    let type_info = match (type_info.0 as *const TypeInfo).as_ref() {
-        Some(type_info) => type_info,
-        None => return ErrorHandle::new("Invalid argument: 'type_info' is null pointer."),
-    };
-
-    let align = match align.as_mut() {
-        Some(align) => align,
-        None => return ErrorHandle::new("Invalid argument: 'align' is null pointer."),
-    };
-
-    *align = type_info.layout.align();
-
-    ErrorHandle::default()
-}
-
-/// An enum containing C-style handles a `TypeInfo`'s data.
-/// cbindgen:prefix-with-name=true
-#[repr(u8)]
-#[derive(Clone, Copy, Debug)]
-pub enum TypeInfoData {
-    /// Primitive types (i.e. `()`, `bool`, `float`, `int`, etc.)
-    Primitive(abi::Guid),
-    /// Struct types (i.e. record, tuple, or unit structs)
-    Struct(StructInfoHandle),
-    /// A pointer type
-    Pointer(PointerInfoData),
-}
-
-/// A pointer to another type.
-#[repr(C)]
-#[derive(Clone, Copy, Debug)]
-pub struct PointerInfoData {
-    //pointee: TypeInfoHandle,
-    mutable: bool,
-}
-
-impl TypeInfoData {
-    /// Whether the type is a primitive.
-    #[cfg(test)]
-    fn is_primitive(&self) -> bool {
-        matches!(*self, TypeInfoData::Primitive(_))
-    }
-
-    /// Whether the type is a struct.
-    #[cfg(test)]
-    fn is_struct(&self) -> bool {
-        matches!(*self, TypeInfoData::Struct(_))
-    }
-
-    /// Returns the C-style handle to the struct information, if available.
-    #[cfg(test)]
-    pub(crate) fn as_struct(&self) -> Option<StructInfoHandle> {
-        if let TypeInfoData::Struct(handle) = self {
-            Some(*handle)
-        } else {
-            None
-        }
-    }
-}
-
-/// Retrieves the type's data.
-///
-/// # Safety
-///
-/// The original `TypeInfoHandle` needs to stay alive as long as the `TypeInfoData` lives. The
-/// `TypeInfoData` is destroyed at the same time as the `TypeInfo`. A `TypeInfo` might be destroyed
-/// through a call to [`mun_type_info_decrement_strong_count`].
-#[no_mangle]
-pub unsafe extern "C" fn mun_type_info_data(
-    type_info: TypeInfoHandle,
-    type_info_data: *mut TypeInfoData,
-) -> ErrorHandle {
-    let type_info = match (type_info.0 as *const TypeInfo).as_ref() {
-        Some(type_info) => type_info,
-        None => return ErrorHandle::new("Invalid argument: 'type_info' is null pointer."),
-    };
-
-    let type_info_data = match type_info_data.as_mut() {
-        Some(type_info_data) => type_info_data,
-        None => return ErrorHandle::new("Invalid argument: 'type_info_data' is null pointer."),
-    };
-
-    *type_info_data = match &type_info.data {
-        memory::TypeInfoData::Primitive(guid) => TypeInfoData::Primitive(*guid),
-        memory::TypeInfoData::Struct(s) => {
-            TypeInfoData::Struct(StructInfoHandle(s as *const StructInfo as *const c_void))
-        }
-        memory::TypeInfoData::Pointer(pointer) => {
-            TypeInfoData::Pointer(PointerInfoData {
-                //pointee: TypeInfoHandle(Arc::into_raw(pointer.pointee.clone()) as _),
-                mutable: pointer.mutable,
-            })
-        }
-    };
-
-    ErrorHandle::default()
-}
-
-/// Deallocates an span of `TypeInfo` that was allocated by the runtime.
-///
-/// # Safety
-///
-/// This function receives a span as parameter. Only when the spans data pointer is not null, its
-/// content will be deallocated. Passing pointers to invalid data or memory allocated by other
-/// processes, will lead to undefined behavior.
-#[no_mangle]
-pub unsafe extern "C" fn mun_type_info_span_destroy(array_handle: TypeInfoSpan) -> bool {
-    if array_handle.data.is_null() {
-        return false;
-    }
-
-    let data = array_handle.data as *mut *const TypeInfo;
-    let types = Vec::from_raw_parts(data, array_handle.len, array_handle.len);
-
-    types.into_iter().for_each(|ty| {
-        let _drop = Arc::from_raw(ty);
-    });
-
-    true
-}
-
-/// Types of primitives supported by Mun.
-/// cbindgen:prefix-with-name=true
-#[repr(u8)]
-#[derive(Clone, Copy)]
-#[allow(missing_docs)]
-pub enum PrimitiveType {
-    Bool,
-    U8,
-    U16,
-    U32,
-    U64,
-    U128,
-    I8,
-    I16,
-    I32,
-    I64,
-    I128,
-    F32,
-    F64,
-    Empty,
-    Void,
-}
-
-/// Returns a TypeInfoHandle that represents the specified primitive type.
-#[no_mangle]
-pub extern "C" fn mun_type_info_primitive(primitive_type: PrimitiveType) -> TypeInfoHandle {
-    match primitive_type {
-        PrimitiveType::Bool => bool::type_info(),
-        PrimitiveType::U8 => u8::type_info(),
-        PrimitiveType::U16 => u16::type_info(),
-        PrimitiveType::U32 => u32::type_info(),
-        PrimitiveType::U64 => u64::type_info(),
-        PrimitiveType::U128 => u128::type_info(),
-        PrimitiveType::I8 => i8::type_info(),
-        PrimitiveType::I16 => i16::type_info(),
-        PrimitiveType::I32 => i32::type_info(),
-        PrimitiveType::I64 => i64::type_info(),
-        PrimitiveType::I128 => i128::type_info(),
-        PrimitiveType::F32 => f32::type_info(),
-        PrimitiveType::F64 => f64::type_info(),
-        PrimitiveType::Empty => <()>::type_info(),
-        PrimitiveType::Void => <std::ffi::c_void>::type_info(),
-    }
-    .clone()
-    .into()
-}
-
-#[cfg(test)]
-pub(crate) mod tests {
-    use super::*;
-    use crate::function_info::mun_function_info_argument_types;
-    use crate::function_info::tests::get_fake_function_info;
-    use crate::{
-        error::mun_error_destroy,
-        runtime::{mun_runtime_get_type_info_by_name, RuntimeHandle},
-        test_util::TestDriver,
-    };
-    use std::{
-        ffi::{CStr, CString},
-        mem::{self, MaybeUninit},
-        sync::Arc,
-    };
-
-    pub(crate) fn get_type_info_by_name<T: Into<Vec<u8>>>(
-        runtime: RuntimeHandle,
-        type_name: T,
-    ) -> TypeInfoHandle {
-        let type_name = CString::new(type_name).expect("Invalid type name");
-        let mut has_type_info = MaybeUninit::uninit();
-        let mut type_info = MaybeUninit::uninit();
-        let handle = unsafe {
-            mun_runtime_get_type_info_by_name(
-                runtime,
-                type_name.as_ptr(),
-                has_type_info.as_mut_ptr(),
-                type_info.as_mut_ptr(),
-            )
-        };
-        assert_eq!(handle.0, ptr::null());
-
-        let has_type_info = unsafe { has_type_info.assume_init() };
-        assert!(has_type_info);
-
-        unsafe { type_info.assume_init() }
-    }
-
-    #[test]
-    fn test_type_info_decrement_strong_count_invalid_type_info() {
-        assert!(!unsafe { mun_type_info_decrement_strong_count(TypeInfoHandle::null()) });
-    }
-
-    #[test]
-    fn test_type_info_decrement_strong_count() {
-        let driver = TestDriver::new(
-            r#"
-        pub fn main() -> i32 { 12345 }
-    "#,
-        );
-
-        let type_info = get_type_info_by_name(driver.runtime, "core::i32");
-
-        let type_info_arc = unsafe { Arc::from_raw(type_info.0 as *const TypeInfo) };
-        let strong_count = Arc::strong_count(&type_info_arc);
-        assert!(strong_count > 0);
-
-        assert!(unsafe { mun_type_info_decrement_strong_count(type_info) });
-        assert_eq!(Arc::strong_count(&type_info_arc), strong_count - 1);
-
-        mem::forget(type_info_arc);
-    }
-
-    #[test]
-    fn test_type_info_increment_strong_count_invalid_type_info() {
-        assert_eq!(
-            unsafe { mun_type_info_increment_strong_count(TypeInfoHandle::null()) },
-            false
-        );
-    }
-
-    #[test]
-    fn test_type_info_increment_strong_count() {
-        let driver = TestDriver::new(
-            r#"
-        struct Foo;
-    "#,
-        );
-
-        let type_info = get_type_info_by_name(driver.runtime, "Foo");
-
-        let type_info_arc = unsafe { Arc::from_raw(type_info.0 as *const TypeInfo) };
-        let strong_count = Arc::strong_count(&type_info_arc);
-        assert!(strong_count > 0);
-
-        assert!(unsafe { mun_type_info_increment_strong_count(type_info) },);
-        assert_eq!(Arc::strong_count(&type_info_arc), strong_count + 1);
-
-        mem::forget(type_info_arc);
-    }
-
-    #[test]
-    fn test_type_info_name_invalid_type_info() {
-        let fn_ptr = unsafe { mun_type_info_name(TypeInfoHandle::null()) };
-        assert_eq!(fn_ptr, ptr::null());
-    }
-
-    #[test]
-    fn test_type_info_name() {
-        let driver = TestDriver::new(
-            r#"
-        pub fn main() -> i32 { 12345 }
-    "#,
-        );
-
-        let type_info = get_type_info_by_name(driver.runtime, "core::i32");
-        let name = unsafe { mun_type_info_name(type_info) };
-        assert_ne!(name, ptr::null());
-
-        let name = unsafe { CStr::from_ptr(name) }
-            .to_str()
-            .expect("Invalid type name.");
-
-        assert_eq!(name, "core::i32");
-    }
-
-    #[test]
-    fn test_type_info_size_invalid_type_info() {
-        let handle = unsafe { mun_type_info_size(TypeInfoHandle::null(), ptr::null_mut()) };
-        assert_ne!(handle.0, ptr::null());
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'type_info' is null pointer."
-        );
-
-        unsafe { mun_error_destroy(handle) };
-    }
-
-    #[test]
-    fn test_type_info_size_invalid_size() {
-        let driver = TestDriver::new(
-            r#"
-        pub fn main() -> i32 { 12345 }
-    "#,
-        );
-
-        let type_info = get_type_info_by_name(driver.runtime, "core::i32");
-        let handle = unsafe { mun_type_info_size(type_info, ptr::null_mut()) };
-        assert_ne!(handle.0, ptr::null());
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'size' is null pointer."
-        );
-
-        unsafe { mun_error_destroy(handle) };
-    }
-
-    #[test]
-    fn test_type_info_size() {
-        let driver = TestDriver::new(
-            r#"
-        pub fn main() -> i32 { 12345 }
-    "#,
-        );
-
-        let type_info = get_type_info_by_name(driver.runtime, "core::i32");
-        let mut size = MaybeUninit::uninit();
-        let handle = unsafe { mun_type_info_size(type_info, size.as_mut_ptr()) };
-        assert_eq!(handle.0, ptr::null());
-
-        let size = unsafe { size.assume_init() };
-        assert_eq!(size, mem::size_of::<i32>());
-    }
-
-    #[test]
-    fn test_type_info_align_invalid_type_info() {
-        let handle = unsafe { mun_type_info_align(TypeInfoHandle::null(), ptr::null_mut()) };
-        assert_ne!(handle.0, ptr::null());
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'type_info' is null pointer."
-        );
-
-        unsafe { mun_error_destroy(handle) };
-    }
-
-    #[test]
-    fn test_type_info_align_invalid_align() {
-        let driver = TestDriver::new(
-            r#"
-        pub fn main() -> i32 { 12345 }
-    "#,
-        );
-
-        let type_info = get_type_info_by_name(driver.runtime, "core::i32");
-        let handle = unsafe { mun_type_info_align(type_info, ptr::null_mut()) };
-        assert_ne!(handle.0, ptr::null());
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'align' is null pointer."
-        );
-
-        unsafe { mun_error_destroy(handle) };
-    }
-
-    #[test]
-    fn test_type_info_align() {
-        let driver = TestDriver::new(
-            r#"
-        pub fn main() -> i32 { 12345 }
-    "#,
-        );
-
-        let type_info = get_type_info_by_name(driver.runtime, "core::i32");
-        let mut align = MaybeUninit::uninit();
-        let handle = unsafe { mun_type_info_align(type_info, align.as_mut_ptr()) };
-        assert_eq!(handle.0, ptr::null());
-
-        let align = unsafe { align.assume_init() };
-        assert_eq!(align, mem::align_of::<i32>());
-    }
-
-    #[test]
-    fn test_type_info_data_invalid_type_info() {
-        let handle = unsafe { mun_type_info_data(TypeInfoHandle::null(), ptr::null_mut()) };
-        assert_ne!(handle.0, ptr::null());
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'type_info' is null pointer."
-        );
-
-        unsafe { mun_error_destroy(handle) };
-    }
-
-    #[test]
-    fn test_type_info_data_invalid_type_info_data() {
-        let driver = TestDriver::new(
-            r#"
-        pub fn main() -> i32 { 12345 }
-    "#,
-        );
-
-        let type_info = get_type_info_by_name(driver.runtime, "core::i32");
-        let handle = unsafe { mun_type_info_data(type_info, ptr::null_mut()) };
-        assert_ne!(handle.0, ptr::null());
-
-        let message = unsafe { CStr::from_ptr(handle.0) };
-        assert_eq!(
-            message.to_str().unwrap(),
-            "Invalid argument: 'type_info_data' is null pointer."
-        );
-
-        unsafe { mun_error_destroy(handle) };
-    }
-
-    #[test]
-    fn test_type_info_data_primitive() {
-        let driver = TestDriver::new(
-            r#"
-        pub fn main() -> i32 { 12345 }
-    "#,
-        );
-
-        let type_info = get_type_info_by_name(driver.runtime, "core::i32");
-        let mut type_info_data = MaybeUninit::uninit();
-        let handle = unsafe { mun_type_info_data(type_info, type_info_data.as_mut_ptr()) };
-        assert_eq!(handle.0, ptr::null());
-
-        let type_info_data = unsafe { type_info_data.assume_init() };
-        assert!(type_info_data.is_primitive());
-        assert!(!type_info_data.is_struct());
-    }
-
-    #[test]
-    fn test_type_info_data_struct() {
-        let driver = TestDriver::new(
-            r#"
-            pub struct Foo;
-
-            pub fn main() -> Foo { Foo }
-            "#,
-        );
-
-        let type_info = get_type_info_by_name(driver.runtime, "Foo");
-        let mut type_info_data = MaybeUninit::uninit();
-        let handle = unsafe { mun_type_info_data(type_info, type_info_data.as_mut_ptr()) };
-        assert_eq!(handle.0, ptr::null());
-
-        let type_info_data = unsafe { type_info_data.assume_init() };
-        assert!(!type_info_data.is_primitive());
-        assert!(type_info_data.is_struct());
-    }
-
-    #[test]
-    fn test_type_info_span_destroy() {
-        let driver = TestDriver::new(
-            r#"
-        pub fn add(a: i32, b: i32) -> i32 { a + b }
-        pub fn empty() -> i32 { 0 }
-    "#,
-        );
-
-        let fn_info = get_fake_function_info(driver.runtime, "add");
-        let mut arg_types = MaybeUninit::uninit();
-        let handle = unsafe { mun_function_info_argument_types(fn_info, arg_types.as_mut_ptr()) };
-        assert_eq!(handle.0, ptr::null());
-
-        let arg_types = unsafe { arg_types.assume_init() };
-        assert!(unsafe { mun_type_info_span_destroy(arg_types) });
-
-        let fn_info = get_fake_function_info(driver.runtime, "empty");
-        let mut arg_types = MaybeUninit::uninit();
-        let handle = unsafe { mun_function_info_argument_types(fn_info, arg_types.as_mut_ptr()) };
-        assert_eq!(handle.0, ptr::null());
-
-        let arg_types = unsafe { arg_types.assume_init() };
-        assert!(!unsafe { mun_type_info_span_destroy(arg_types) });
-    }
-
-    #[test]
-    fn test_type_info_primitive() {
-        let type_info_handle = mun_type_info_primitive(PrimitiveType::F32);
-        assert_ne!(type_info_handle.0, ptr::null());
-
-        let type_info: Arc<TypeInfo> = unsafe { Arc::from_raw(type_info_handle.0 as _) };
-        assert_eq!(&type_info, f32::type_info());
-    }
-
-    #[test]
-    fn test_type_info_pointer_type_invalid_pointer() {
-        let result = unsafe { mun_type_info_pointer_type(TypeInfoHandle::null(), true) };
-        assert_eq!(result.0, ptr::null());
-    }
-
-    #[test]
-    fn test_type_info_pointer_type() {
-        // Get the f32 type
-        let f32_type_info_handle = mun_type_info_primitive(PrimitiveType::F32);
-        assert_ne!(f32_type_info_handle.0, ptr::null());
-
-        // Create a mutable pointer to f32
-        let f32_pointer_type_info_handle =
-            unsafe { mun_type_info_pointer_type(f32_type_info_handle, true) };
-        assert_ne!(f32_pointer_type_info_handle.0, ptr::null());
-
-        // Verify that its the same type as the rust type
-        let f32_pointer_arc = unsafe { Arc::from_raw(f32_pointer_type_info_handle.0 as _) };
-        assert_eq!(&f32_pointer_arc, <*mut f32>::type_info());
-
-        // Create a const pointer to f32
-        let f32_pointer_type_info_handle =
-            unsafe { mun_type_info_pointer_type(f32_type_info_handle, false) };
-        assert_ne!(f32_pointer_type_info_handle.0, ptr::null());
-
-        // Verify that its the same type as the rust type
-        let f32_pointer_arc = unsafe { Arc::from_raw(f32_pointer_type_info_handle.0 as _) };
-        assert_eq!(&f32_pointer_arc, <*const f32>::type_info());
-
-        // Drop the f32 type
-        unsafe { mun_type_info_decrement_strong_count(f32_type_info_handle) };
-    }
-
-    #[test]
-    fn test_type_info_eq_invalid_ptr() {
-        let f32_type_info_handle = mun_type_info_primitive(PrimitiveType::F32);
-        assert_ne!(f32_type_info_handle.0, ptr::null());
-
-        assert!(!unsafe { mun_type_info_eq(TypeInfoHandle::null(), TypeInfoHandle::null()) });
-        assert!(!unsafe { mun_type_info_eq(f32_type_info_handle, TypeInfoHandle::null()) });
-        assert!(!unsafe { mun_type_info_eq(TypeInfoHandle::null(), f32_type_info_handle) });
-    }
-
-    #[test]
-    fn test_type_info_eq() {
-        let f32_type_info_handle = mun_type_info_primitive(PrimitiveType::F32);
-        assert_ne!(f32_type_info_handle.0, ptr::null());
-
-        let f32_type_info_handle_2 = mun_type_info_primitive(PrimitiveType::F32);
-        assert_ne!(f32_type_info_handle_2.0, ptr::null());
-
-        let f64_type_info_handle = mun_type_info_primitive(PrimitiveType::F64);
-        assert_ne!(f64_type_info_handle.0, ptr::null());
-
-        assert!(unsafe { mun_type_info_eq(f32_type_info_handle, f32_type_info_handle) });
-        assert!(unsafe { mun_type_info_eq(f32_type_info_handle, f32_type_info_handle_2) });
-        assert!(unsafe { mun_type_info_eq(f64_type_info_handle, f64_type_info_handle) });
-        assert!(!unsafe { mun_type_info_eq(f64_type_info_handle, f32_type_info_handle) });
-    }
-}
diff --git a/examples/buoyancy/main.cc b/examples/buoyancy/main.cc
index f5caa3d85..18d134c13 100644
--- a/examples/buoyancy/main.cc
+++ b/examples/buoyancy/main.cc
@@ -10,7 +10,7 @@ void log_f32(float value) { std::cout << std::to_string(value) << std::endl; }
 // How to run?
 // 1. On the CLI, navigate to the `example-cpp` directory.
 // 2. Run the compiler daemon from the CLI:
-//    `/path/to/mun build resources/buoyancy.mun --watch`
+//    `/path/to/mun build --manifest-path examples/buoyancy/mun.toml --watch`
 // 3. Run the application from the CLI:
 //    `main /path/to/buoyancy.munlib`
 int main(int argc, char* argv[]) {
@@ -35,21 +35,20 @@ int main(int argc, char* argv[]) {
             std::this_thread::sleep_until(previous + FRAME_TIME);
 
             const auto now = clock_t::now();
-            const auto elapsed =
-                std::chrono::duration_cast<fsec_t>(now - previous);
+            const auto elapsed = std::chrono::duration_cast<fsec_t>(now - previous);
 
             mun::invoke_fn<void>(*runtime, "sim_update", ctx, elapsed.count()).wait();
             previous = now;
 
             mun::Error update_error;
-            if (!runtime->update(&update_error) && update_error) {
+            if (!runtime->update(&update_error) && update_error.is_error()) {
                 std::cerr << "Failed to update runtime due to error: "
-                          << update_error.message() << std::endl;
+                          << update_error.message().value() << std::endl;
             }
         }
     }
 
-    std::cerr << "Failed to construct Mun runtime due to error: "
-              << error.message() << std::endl;
+    std::cerr << "Failed to construct Mun runtime due to error: " << error.message().value()
+              << std::endl;
     return 2;
 }


Test patch:
diff --git a/cpp/tests/extern.cc b/cpp/tests/extern.cc
index 430c42051..4c6dea235 100644
--- a/cpp/tests/extern.cc
+++ b/cpp/tests/extern.cc
@@ -19,7 +19,7 @@ TEST_CASE("functions must be inserted into the runtime", "[extern]") {
     mun::Error err;
     auto runtime = mun::make_runtime(get_munlib_path("extern/target/mod.munlib"), options, &err);
     REQUIRE(!runtime);
-    REQUIRE(err);
+    REQUIRE(err.is_error());
 }
 
 TEST_CASE("function must have correct signature", "[extern]") {
@@ -29,7 +29,7 @@ TEST_CASE("function must have correct signature", "[extern]") {
     mun::Error err;
     auto runtime = mun::make_runtime(get_munlib_path("extern/target/mod.munlib"), options, &err);
     REQUIRE(!runtime);
-    REQUIRE(err);
+    REQUIRE(err.is_error());
 }
 
 TEST_CASE("functions can be inserted into the runtime", "[extern]") {
@@ -39,8 +39,8 @@ TEST_CASE("functions can be inserted into the runtime", "[extern]") {
     mun::Error err;
     auto runtime = mun::make_runtime(get_munlib_path("extern/target/mod.munlib"), options, &err);
     if (!runtime) {
-        REQUIRE(err);
-        FAIL(err.message());
+        REQUIRE(err.is_error());
+        FAIL(err.message().value());
     }
 
     REQUIRE(mun::invoke_fn<uint32_t, uint32_t, uint32_t>(*runtime, "main", 90, 2648).unwrap() ==
diff --git a/cpp/tests/marshal.cc b/cpp/tests/marshal.cc
index 17720005b..08a43b9a6 100644
--- a/cpp/tests/marshal.cc
+++ b/cpp/tests/marshal.cc
@@ -10,69 +10,69 @@ inline std::string get_munlib_path(std::string_view name) {
     return ss.str();
 }
 
-#define TEST_MARSHALLING(ty, lhs, rhs, expected)                                          \
-    TEST_CASE("function can marshal " #ty, "[marshal]") {                                 \
-        mun::Error err;                                                                   \
-        if (auto runtime =                                                                \
+#define TEST_MARSHALLING(ty, lhs, rhs, expected)                                             \
+    TEST_CASE("function can marshal " #ty, "[marshal]") {                                    \
+        mun::Error err;                                                                      \
+        if (auto runtime =                                                                   \
                 mun::make_runtime(get_munlib_path("marshal/target/mod.munlib"), {}, &err)) { \
-            REQUIRE(!err);                                                                \
-                                                                                          \
-            const ty a = (lhs), b = (rhs);                                                \
-            auto res = mun::invoke_fn<ty>(*runtime, "marshal_" #ty, a, b);                \
-            REQUIRE(res.is_ok());                                                         \
-            REQUIRE(res.wait() == (expected));                                            \
-        } else {                                                                          \
-            REQUIRE(err);                                                                 \
-            FAIL(err.message());                                                          \
-        }                                                                                 \
-    }                                                                                     \
-    TEST_CASE("struct can get, set, and replace " #ty, "[marshal]") {                     \
-        mun::Error err;                                                                   \
-        if (auto runtime =                                                                \
+            REQUIRE(err.is_ok());                                                            \
+                                                                                             \
+            const ty a = (lhs), b = (rhs);                                                   \
+            auto res = mun::invoke_fn<ty>(*runtime, "marshal_" #ty, a, b);                   \
+            REQUIRE(res.is_ok());                                                            \
+            REQUIRE(res.wait() == (expected));                                               \
+        } else {                                                                             \
+            REQUIRE(err.is_error());                                                         \
+            FAIL(err.message().value());                                                     \
+        }                                                                                    \
+    }                                                                                        \
+    TEST_CASE("struct can get, set, and replace " #ty, "[marshal]") {                        \
+        mun::Error err;                                                                      \
+        if (auto runtime =                                                                   \
                 mun::make_runtime(get_munlib_path("marshal/target/mod.munlib"), {}, &err)) { \
-            REQUIRE(!err);                                                                \
-                                                                                          \
-            const ty a = (lhs), b = (rhs);                                                \
-            auto res = mun::invoke_fn<mun::StructRef>(*runtime, "new_" #ty, a, b);        \
-            REQUIRE(res.is_ok());                                                         \
-                                                                                          \
-            auto s = res.wait();                                                          \
-            {                                                                             \
-                const auto first = s.get<ty>("0");                                        \
-                REQUIRE(first.has_value());                                               \
-                REQUIRE(*first == a);                                                     \
-            }                                                                             \
-            {                                                                             \
-                const auto second = s.get<ty>("1");                                       \
-                REQUIRE(second.has_value());                                              \
-                REQUIRE(*second == b);                                                    \
-            }                                                                             \
-            REQUIRE(s.set("0", b));                                                       \
-            REQUIRE(s.set("1", a));                                                       \
-            {                                                                             \
-                const auto first = s.replace<ty>("0", a);                                 \
-                REQUIRE(first.has_value());                                               \
-                REQUIRE(*first == b);                                                     \
-            }                                                                             \
-            {                                                                             \
-                const auto second = s.replace<ty>("1", b);                                \
-                REQUIRE(second.has_value());                                              \
-                REQUIRE(*second == a);                                                    \
-            }                                                                             \
-            {                                                                             \
-                const auto first = s.get<ty>("0");                                        \
-                REQUIRE(first.has_value());                                               \
-                REQUIRE(*first == a);                                                     \
-            }                                                                             \
-            {                                                                             \
-                const auto second = s.get<ty>("1");                                       \
-                REQUIRE(second.has_value());                                              \
-                REQUIRE(*second == b);                                                    \
-            }                                                                             \
-        } else {                                                                          \
-            REQUIRE(err);                                                                 \
-            FAIL(err.message());                                                          \
-        }                                                                                 \
+            REQUIRE(err.is_ok());                                                            \
+                                                                                             \
+            const ty a = (lhs), b = (rhs);                                                   \
+            auto res = mun::invoke_fn<mun::StructRef>(*runtime, "new_" #ty, a, b);           \
+            REQUIRE(res.is_ok());                                                            \
+                                                                                             \
+            auto s = res.wait();                                                             \
+            {                                                                                \
+                const auto first = s.get<ty>("0");                                           \
+                REQUIRE(first.has_value());                                                  \
+                REQUIRE(*first == a);                                                        \
+            }                                                                                \
+            {                                                                                \
+                const auto second = s.get<ty>("1");                                          \
+                REQUIRE(second.has_value());                                                 \
+                REQUIRE(*second == b);                                                       \
+            }                                                                                \
+            REQUIRE(s.set("0", b));                                                          \
+            REQUIRE(s.set("1", a));                                                          \
+            {                                                                                \
+                const auto first = s.replace<ty>("0", a);                                    \
+                REQUIRE(first.has_value());                                                  \
+                REQUIRE(*first == b);                                                        \
+            }                                                                                \
+            {                                                                                \
+                const auto second = s.replace<ty>("1", b);                                   \
+                REQUIRE(second.has_value());                                                 \
+                REQUIRE(*second == a);                                                       \
+            }                                                                                \
+            {                                                                                \
+                const auto first = s.get<ty>("0");                                           \
+                REQUIRE(first.has_value());                                                  \
+                REQUIRE(*first == a);                                                        \
+            }                                                                                \
+            {                                                                                \
+                const auto second = s.get<ty>("1");                                          \
+                REQUIRE(second.has_value());                                                 \
+                REQUIRE(*second == b);                                                       \
+            }                                                                                \
+        } else {                                                                             \
+            REQUIRE(err.is_error());                                                         \
+            FAIL(err.message().value());                                                     \
+        }                                                                                    \
     }
 
 // TODO: Add 128-bit integers
@@ -94,7 +94,7 @@ TEST_MARSHALLING(uint64_t, 1, 64, 1 + 64);
 TEST_CASE("struct can get, set, and replace struct", "[marshal]") {
     mun::Error err;
     if (auto runtime = mun::make_runtime(get_munlib_path("marshal/target/mod.munlib"), {}, &err)) {
-        REQUIRE(!err);
+        REQUIRE(err.is_ok());
 
         float a = -3.14f, b = 6.28f;
         auto gc_struct_res = mun::invoke_fn<mun::StructRef>(*runtime, "new_gc_struct", a, b);
@@ -222,7 +222,7 @@ TEST_CASE("struct can get, set, and replace struct", "[marshal]") {
             REQUIRE(*value4_1 == b);
         }
     } else {
-        REQUIRE(err);
-        FAIL(err.message());
+        REQUIRE(err.is_error());
+        FAIL(err.message().value());
     }
 }
diff --git a/cpp/tests/runtime.cc b/cpp/tests/runtime.cc
index 230c74de4..754e1d600 100644
--- a/cpp/tests/runtime.cc
+++ b/cpp/tests/runtime.cc
@@ -14,10 +14,10 @@ TEST_CASE("runtime can be constructed", "[runtime]") {
     mun::Error err;
     if (auto runtime =
             mun::make_runtime(get_munlib_path("fibonacci/target/mod.munlib"), {}, &err)) {
-        REQUIRE(!err);
+        REQUIRE(err.is_ok());
     } else {
-        REQUIRE(err);
-        FAIL(err.message());
+        REQUIRE(err.is_error());
+        FAIL(err.message().value());
     }
 }
 
@@ -25,18 +25,18 @@ TEST_CASE("runtime can find `FunctionInfo`", "[runtime]") {
     mun::Error err;
     if (auto runtime =
             mun::make_runtime(get_munlib_path("fibonacci/target/mod.munlib"), {}, &err)) {
-        REQUIRE(!err);
+        REQUIRE(err.is_ok());
         REQUIRE(runtime.has_value());
 
         if (auto function_info = runtime->find_function_info("fibonacci", &err)) {
-            REQUIRE(!err);
+            REQUIRE(err.is_ok());
         } else {
-            REQUIRE(err);
-            FAIL(err.message());
+            REQUIRE(err.is_error());
+            FAIL(err.message().value());
         }
     } else {
-        REQUIRE(err);
-        FAIL(err.message());
+        REQUIRE(err.is_error());
+        FAIL(err.message().value());
     }
 }
 
@@ -45,23 +45,23 @@ TEST_CASE("runtime can update", "[runtime]") {
     mun::Error err;
     if (auto runtime =
             mun::make_runtime(get_munlib_path("fibonacci/target/mod.munlib"), {}, &err)) {
-        REQUIRE(!err);
+        REQUIRE(err.is_ok());
 
         runtime->update(&err);
-        if (err) {
-            FAIL(err.message());
+        if (err.is_error()) {
+            FAIL(err.message().value());
         }
-        REQUIRE(!err);
+        REQUIRE(err.is_ok());
     } else {
-        REQUIRE(err);
-        FAIL(err.message());
+        REQUIRE(err.is_error());
+        FAIL(err.message().value());
     }
 }
 
 TEST_CASE("runtime can garbage collect", "[runtime]") {
     mun::Error err;
     if (auto runtime = mun::make_runtime(get_munlib_path("marshal/target/mod.munlib"), {}, &err)) {
-        REQUIRE(!err);
+        REQUIRE(err.is_ok());
         {
             auto res = mun::invoke_fn<mun::StructRef>(*runtime, "new_bool", true, false);
             REQUIRE(res.is_ok());
@@ -69,7 +69,7 @@ TEST_CASE("runtime can garbage collect", "[runtime]") {
         }
         REQUIRE(runtime->gc_collect());
     } else {
-        REQUIRE(err);
-        FAIL(err.message());
+        REQUIRE(err.is_error());
+        FAIL(err.message().value());
     }
 }
diff --git a/crates/mun_memory/tests/diff/structs.rs b/crates/mun_memory/tests/diff/structs.rs
index 0ac8cce9b..5f289d098 100644
--- a/crates/mun_memory/tests/diff/structs.rs
+++ b/crates/mun_memory/tests/diff/structs.rs
@@ -1,13 +1,11 @@
-use std::sync::Arc;
-
 use crate::{diff::util::*, fake_struct};
 use mun_memory::{
     diff::{diff, Diff, FieldDiff, FieldEditKind},
     type_table::TypeTable,
-    TypeInfo,
+    Type,
 };
 
-fn assert_eq_struct(result: &[Arc<TypeInfo>], expected: &[Arc<TypeInfo>]) {
+fn assert_eq_struct(result: &[Type], expected: &[Type]) {
     assert_eq!(result.len(), expected.len());
     for (lhs, rhs) in result.iter().zip(expected.iter()) {
         assert_eq!(lhs, rhs);
diff --git a/crates/mun_memory/tests/diff/util.rs b/crates/mun_memory/tests/diff/util.rs
index 751a7fdbf..0b1b774a8 100644
--- a/crates/mun_memory/tests/diff/util.rs
+++ b/crates/mun_memory/tests/diff/util.rs
@@ -1,11 +1,10 @@
 #![allow(dead_code)]
+
 use mun_memory::{
     diff::{myers, Diff, FieldDiff, FieldEditKind},
-    FieldInfo, StructInfo, TypeInfo,
+    Field, StructType, StructTypeBuilder, Type,
 };
-use std::sync::Arc;
-
-use crate::util::{fake_layout, struct_guid};
+use std::collections::VecDeque;
 
 pub fn apply_myers_diff<T: Clone + Eq>(old: &[T], new: &[T], diff: Vec<myers::Diff>) -> Vec<T> {
     let mut combined: Vec<_> = old.to_vec();
@@ -23,12 +22,8 @@ pub fn apply_myers_diff<T: Clone + Eq>(old: &[T], new: &[T], diff: Vec<myers::Di
     combined
 }
 
-pub(crate) fn apply_diff(
-    old: &[Arc<TypeInfo>],
-    new: &[Arc<TypeInfo>],
-    diff: Vec<Diff>,
-) -> Vec<Arc<TypeInfo>> {
-    let mut combined: Vec<Arc<TypeInfo>> = old.to_vec();
+pub(crate) fn apply_diff(old: &[Type], new: &[Type], diff: Vec<Diff>) -> Vec<Type> {
+    let mut combined: Vec<Type> = old.to_vec();
     for diff in diff.iter().rev() {
         match diff {
             Diff::Delete { index } => {
@@ -42,7 +37,7 @@ pub(crate) fn apply_diff(
                 let old_ty = unsafe { combined.get_unchecked_mut(*old_index) };
                 let new_ty = unsafe { new.get_unchecked(*new_index) };
 
-                let mut combined_struct_info = old_ty
+                let combined_struct_info = old_ty
                     .as_struct()
                     .expect("edit diffs can only be applied on structs")
                     .clone();
@@ -50,18 +45,12 @@ pub(crate) fn apply_diff(
                     .as_struct()
                     .expect("edit diffs can only be applied on structs");
 
-                apply_struct_mapping(
-                    &old_ty.name,
-                    &mut combined_struct_info,
+                *old_ty = apply_struct_mapping(
+                    old_ty.name(),
+                    combined_struct_info,
                     new_struct_info,
                     diff,
                 );
-
-                *old_ty = TypeInfo::new_struct(
-                    &old_ty.name,
-                    fake_layout(&combined_struct_info),
-                    combined_struct_info,
-                );
             }
             Diff::Move { old_index, .. } => {
                 combined.remove(*old_index);
@@ -90,19 +79,24 @@ pub(crate) fn apply_diff(
 
 fn apply_struct_mapping(
     name: &str,
-    old_struct: &mut StructInfo,
-    new_struct: &StructInfo,
+    old_struct: StructType,
+    new_struct: StructType,
     mapping: &[FieldDiff],
-) {
-    let mut combined = old_struct.clone();
+) -> Type {
+    let mut fields: VecDeque<_> = old_struct
+        .fields()
+        .iter()
+        .map(|f| (f.name().to_owned(), f.ty()))
+        .collect();
+
     for diff in mapping.iter().rev() {
         match diff {
             FieldDiff::Delete { index } => {
-                combined.fields.remove(*index);
+                fields.remove(*index);
             }
 
             FieldDiff::Move { old_index, .. } => {
-                combined.fields.remove(*old_index);
+                fields.remove(*old_index);
             }
             _ => (),
         }
@@ -117,34 +111,34 @@ fn apply_struct_mapping(
     }
 
     // Sort elements in ascending order of their insertion indices.
-    let mut additions: Vec<(usize, FieldInfo)> = mapping
+    let mut additions: Vec<_> = mapping
         .iter()
         .filter_map(|diff| match diff {
-            FieldDiff::Insert { index } => Some((
-                *index,
-                unsafe { new_struct.fields.get_unchecked(*index) }.clone(),
-            )),
+            FieldDiff::Insert { index } => {
+                let new_field = new_struct.fields().get(*index).unwrap();
+                Some((*index, (new_field.name().to_owned(), new_field.ty())))
+            }
             FieldDiff::Move {
                 old_index,
                 new_index,
                 ..
-            } => Some((
-                *new_index,
-                unsafe { old_struct.fields.get_unchecked(*old_index) }.clone(),
-            )),
+            } => {
+                let field = old_struct.fields().get(*old_index).unwrap();
+                Some((*new_index, (field.name().to_owned(), field.ty())))
+            }
             _ => None,
         })
         .collect();
     additions.sort_by(|a, b| a.0.cmp(&b.0));
 
     for (index, field) in additions {
-        combined.fields.insert(index, field);
+        fields.insert(index, field);
     }
 
-    fn edit_field(kind: &FieldEditKind, old_field: &mut FieldInfo, new_field: &FieldInfo) {
+    fn edit_field(kind: &FieldEditKind, old_field: &mut (String, Type), new_field: Field) {
         match *kind {
-            FieldEditKind::ConvertType => old_field.type_info = new_field.type_info.clone(),
-            FieldEditKind::Rename => old_field.name = new_field.name.to_owned(),
+            FieldEditKind::ConvertType => old_field.1 = new_field.ty(),
+            FieldEditKind::Rename => old_field.0 = new_field.name().to_owned(),
         }
     }
 
@@ -153,8 +147,8 @@ fn apply_struct_mapping(
         match diff {
             FieldDiff::Edit { index, kind } => edit_field(
                 kind,
-                unsafe { combined.fields.get_unchecked_mut(*index) },
-                unsafe { new_struct.fields.get_unchecked(*index) },
+                &mut fields[*index],
+                new_struct.fields().get(*index).unwrap(),
             ),
             FieldDiff::Move {
                 old_index,
@@ -162,13 +156,14 @@ fn apply_struct_mapping(
                 edit: Some(kind),
             } => edit_field(
                 kind,
-                unsafe { combined.fields.get_unchecked_mut(*old_index) },
-                unsafe { new_struct.fields.get_unchecked(*new_index) },
+                &mut fields[*old_index],
+                new_struct.fields().get(*new_index).unwrap(),
             ),
             _ => (),
         }
     }
 
-    *old_struct = combined;
-    old_struct.guid = struct_guid(name, old_struct.fields.iter());
+    StructTypeBuilder::new(name.to_owned())
+        .add_fields(fields)
+        .finish()
 }
diff --git a/crates/mun_memory/tests/gc/alloc.rs b/crates/mun_memory/tests/gc/alloc.rs
index bdb8009ac..d65776ea7 100644
--- a/crates/mun_memory/tests/gc/alloc.rs
+++ b/crates/mun_memory/tests/gc/alloc.rs
@@ -1,7 +1,7 @@
 use super::util::EventAggregator;
 use mun_memory::{
     gc::{Event, GcRootPtr, GcRuntime, MarkSweep},
-    HasStaticTypeInfo,
+    HasStaticType,
 };
 use std::sync::Arc;
 
diff --git a/crates/mun_memory/tests/type_gc.rs b/crates/mun_memory/tests/type_gc.rs
new file mode 100644
index 000000000..fd808cf41
--- /dev/null
+++ b/crates/mun_memory/tests/type_gc.rs
@@ -0,0 +1,35 @@
+use mun_memory::{HasStaticType, StructTypeBuilder, Type};
+
+#[test]
+fn test_type_collection() {
+    // Initially there is no type data
+    let current_stats = Type::collect_unreferenced_type_data();
+    assert_eq!(current_stats.collected_types, 0);
+    assert_eq!(current_stats.remaining_types, 0);
+
+    // Lets get some static type information
+    let _ = i32::type_info();
+    let _ = <()>::type_info();
+    let _ = bool::type_info();
+
+    // The static types should not be collected because they are static.
+    let current_stats = Type::collect_unreferenced_type_data();
+    assert_eq!(current_stats.collected_types, 0);
+    assert_eq!(current_stats.remaining_types, 3);
+
+    // Lets create a custom type that references one of these static types.
+    let foo_type = StructTypeBuilder::new("Foo")
+        .add_field("bar", i32::type_info().clone())
+        .finish();
+
+    // Now one type should be collected because we have a reference to it, one is added
+    let current_stats = Type::collect_unreferenced_type_data();
+    assert_eq!(current_stats.collected_types, 0);
+    assert_eq!(current_stats.remaining_types, 4);
+
+    // Drop the type and see if collection worked
+    drop(foo_type);
+    let current_stats = Type::collect_unreferenced_type_data();
+    assert_eq!(current_stats.collected_types, 1);
+    assert_eq!(current_stats.remaining_types, 3);
+}
diff --git a/crates/mun_memory/tests/util.rs b/crates/mun_memory/tests/util.rs
index f7db41130..729fbb008 100644
--- a/crates/mun_memory/tests/util.rs
+++ b/crates/mun_memory/tests/util.rs
@@ -1,106 +1,15 @@
 #![allow(dead_code)]
 
-use std::alloc::Layout;
-
-use mun_memory::{FieldInfo, StructInfo, TypeInfo};
-
-/// Generates the Guid string for the struct.
-///
-/// This does not support recursive struct types!
-pub fn struct_guid<'t>(name: &str, fields: impl Iterator<Item = &'t FieldInfo> + 't) -> abi::Guid {
-    let guid_string = struct_guid_string(name, fields);
-    abi::Guid::from_str(&guid_string)
-}
-
-fn struct_guid_string<'t>(name: &str, fields: impl Iterator<Item = &'t FieldInfo> + 't) -> String {
-    let fields: Vec<String> = fields
-        .map(|field| {
-            let ty_string = type_guid_string(&field.type_info);
-            format!("{}: {}", field.name, ty_string)
-        })
-        .collect();
-
-    format!(
-        "struct {name}{{{fields}}}",
-        name = name,
-        fields = fields.join(",")
-    )
-}
-
-fn type_guid_string(type_info: &TypeInfo) -> String {
-    match &type_info.data {
-        mun_memory::TypeInfoData::Struct(s) => {
-            if s.memory_kind == abi::StructMemoryKind::Gc {
-                format!("struct {}", type_info.name)
-            } else {
-                struct_guid_string(&type_info.name, s.fields.iter())
-            }
-        }
-        mun_memory::TypeInfoData::Primitive(_) | mun_memory::TypeInfoData::Pointer(_) => {
-            type_info.name.clone()
-        }
-    }
-}
-
-pub fn fake_layout(struct_info: &StructInfo) -> Layout {
-    let size = struct_info
-        .fields
-        .iter()
-        .map(|field| field.type_info.layout.size())
-        .sum();
-
-    let alignment = struct_info
-        .fields
-        .iter()
-        .map(|field| field.type_info.layout.align())
-        .max()
-        .unwrap();
-
-    Layout::from_size_align(size, alignment).unwrap()
-}
-
 #[macro_export]
 macro_rules! fake_struct {
     ($type_table:expr, $struct_name:expr, $($field_name:expr => $field_ty:ident),+) => {{
-        let mut field_names = Vec::new();
-        let mut field_types = Vec::new();
-
-        $(
-            field_names.push(String::from($field_name));
-            field_types.push($type_table.find_type_info_by_name(format!("core::{}", stringify!($field_ty))).unwrap());
-        )+
-
-        let mut total_size = 0;
-        let field_offsets: Vec<u16> = field_types
-            .iter()
-            .map(|ty| {
-                let offset = total_size;
-                total_size += ty.layout.size();
-                offset as u16
-            })
-            .collect();
-
-        let fields: Vec<_ > = itertools::izip!(field_names, field_types, field_offsets)
-            .map(|(name, type_info, offset)| mun_memory::FieldInfo {
-                name,
-                type_info,
-                offset,
-            })
-            .collect();
-
-        let name = String::from($struct_name);
-        let guid = crate::util::struct_guid(&name, fields.iter());
-
-        let struct_info = mun_memory::StructInfo {
-            guid: guid.clone(),
-            fields,
-            memory_kind: abi::StructMemoryKind::Gc,
-        };
-
-        mun_memory::TypeInfo::new_struct(
-            name,
-            crate::util::fake_layout(&struct_info),
-            struct_info,
-        )
+        mun_memory::StructTypeBuilder::new(String::from($struct_name))
+            $(
+                .add_field(
+                    String::from($field_name),
+                    $type_table.find_type_info_by_name(format!("core::{}", stringify!($field_ty))).unwrap()
+                )
+            )+
+             .finish()
     }};
 }
diff --git a/crates/mun_runtime/tests/marshalling.rs b/crates/mun_runtime/tests/marshalling.rs
index 41756adc9..d07e20917 100644
--- a/crates/mun_runtime/tests/marshalling.rs
+++ b/crates/mun_runtime/tests/marshalling.rs
@@ -410,9 +410,9 @@ fn marshal_struct() {
             .type_info()
             .as_struct()
             .unwrap()
-            .fields
+            .fields()
             .iter()
-            .map(|field| field.name.clone())
+            .map(|field| field.name().to_owned())
             .collect();
 
         let int_value = c2.get::<i64>(&field_names[0]);
diff --git a/crates/mun_runtime/tests/memory.rs b/crates/mun_runtime/tests/memory.rs
index c229e850e..70359a229 100644
--- a/crates/mun_runtime/tests/memory.rs
+++ b/crates/mun_runtime/tests/memory.rs
@@ -1,6 +1,5 @@
 use mun_runtime::StructRef;
 use mun_test::CompileAndRunTestDriver;
-use std::sync::Arc;
 
 #[macro_use]
 mod util;
@@ -1095,14 +1094,11 @@ fn test_type_table() {
         .type_info()
         .as_struct()
         .expect("is not a struct?")
-        .find_field_by_name("bar")
+        .fields()
+        .find_by_name("bar")
         .expect("could not find `bar` field")
-        .type_info
-        .clone();
+        .ty();
 
     // These types should be equal
     assert_eq!(foo_bar_field_type, bar_type);
-
-    // In fact the pointers should be equal!
-    assert!(Arc::ptr_eq(&foo_bar_field_type, &bar_type));
 }
diff --git a/crates/mun_runtime/tests/runtime.rs b/crates/mun_runtime/tests/runtime.rs
index 8a527632e..6af53881c 100644
--- a/crates/mun_runtime/tests/runtime.rs
+++ b/crates/mun_runtime/tests/runtime.rs
@@ -128,3 +128,34 @@ fn arg_missing_bug() {
     );
     driver.unwrap();
 }
+
+#[test]
+fn cyclic_struct() {
+    let driver = CompileAndRunTestDriver::new(
+        r"
+        pub struct Foo {
+            foo: Foo
+        }
+
+        pub struct FooBar {
+            bar: BarFoo
+        }
+
+        pub struct BarFoo {
+            foo: FooBar
+        }
+        ",
+        |builder| builder,
+    )
+    .unwrap();
+
+    let foo_ty = driver.runtime.get_type_info_by_name("Foo").unwrap();
+    let foo_foo_ty = foo_ty
+        .as_struct()
+        .unwrap()
+        .fields()
+        .find_by_name("foo")
+        .unwrap()
+        .ty();
+    assert_eq!(foo_foo_ty, foo_ty);
+}
diff --git a/crates/mun_runtime_capi/src/test_util.rs b/crates/mun_runtime_capi/src/test_util.rs
index 9c8c02966..e321ec1a8 100644
--- a/crates/mun_runtime_capi/src/test_util.rs
+++ b/crates/mun_runtime_capi/src/test_util.rs
@@ -1,13 +1,13 @@
 use compiler::{Config, DisplayColor, Driver, PathOrInline, RelativePathBuf};
 use std::{ffi::CString, io::stderr, path::Path, ptr};
 
-use crate::runtime::{mun_runtime_create, mun_runtime_destroy, RuntimeHandle, RuntimeOptions};
+use crate::runtime::{mun_runtime_create, mun_runtime_destroy, Runtime, RuntimeOptions};
 
 /// Combines a compiler and runtime in one. Use of the TestDriver allows for quick testing of Mun
 /// constructs in the runtime with hot-reloading support.
 pub(crate) struct TestDriver {
     _temp_dir: tempfile::TempDir,
-    pub(crate) runtime: RuntimeHandle,
+    pub(crate) runtime: Runtime,
 }
 
 impl TestDriver {
@@ -45,11 +45,11 @@ impl Drop for TestDriver {
     }
 }
 
-fn make_runtime(lib_path: &Path) -> RuntimeHandle {
+fn make_runtime(lib_path: &Path) -> Runtime {
     let lib_path = lib_path.to_str().expect("Invalid lib path");
     let lib_path = CString::new(lib_path).unwrap();
 
-    let mut handle = RuntimeHandle(ptr::null_mut());
+    let mut handle = Runtime(ptr::null_mut());
     let error = unsafe {
         mun_runtime_create(
             lib_path.as_ptr(),
@@ -70,14 +70,14 @@ macro_rules! test_invalid_runtime {
             paste::item! {
                 #[test]
                 fn [<test_ $name _invalid_runtime>]() {
-                    let runtime = RuntimeHandle(ptr::null_mut());
+                    let runtime = Runtime(ptr::null_mut());
                     let handle =
                         unsafe { [<mun_ $name>](runtime $(, $arg)*) };
 
-                    let message = unsafe { CStr::from_ptr(handle.0) };
+                    let message = unsafe { std::ffi::CStr::from_ptr(handle.0) };
                     assert_eq!(
                         message.to_str().unwrap(),
-                        "Invalid argument: 'runtime' is null pointer."
+                        "invalid argument 'runtime': null pointer"
                     );
 
                     unsafe { mun_error_destroy(handle) };