#[runtime_interface]
Expand description

Attribute macro for transforming a trait declaration into a runtime interface.

A runtime interface is a fixed interface between a Substrate compatible runtime and the native node. This interface is callable from a native and a wasm runtime. The macro will generate the corresponding code for the native implementation and the code for calling from the wasm side to the native implementation.

The macro expects the runtime interface declaration as trait declaration:


#[runtime_interface]
trait Interface {
    /// A function that can be called from native/wasm.
    ///
    /// The implementation given to this function is only compiled on native.
    fn call(data: &[u8]) -> Vec<u8> {
        // Here you could call some rather complex code that only compiles on native or
        // is way faster in native than executing it in wasm.
        Vec::new()
    }
    /// Call function, but different version.
    ///
    /// For new runtimes, only function with latest version is reachable.
    /// But old version (above) is still accessible for old runtimes.
    /// Default version is 1.
    #[version(2)]
    fn call(data: &[u8]) -> Vec<u8> {
        // Here you could call some rather complex code that only compiles on native or
        // is way faster in native than executing it in wasm.
        [17].to_vec()
    }

    /// Call function, different version and only being registered.
    ///
    /// This `register_only` version is only being registered, aka exposed to the runtime,
    /// but the runtime will still use the version 2 of this function. This is useful for when
    /// new host functions should be introduced. Adding new host functions requires that all
    /// nodes have the host functions available, because otherwise they fail at instantiation
    /// of the runtime. With `register_only` the function will not be used when compiling the
    /// runtime, but it will already be there for a future version of the runtime that will
    /// switch to using these host function.
    #[version(3, register_only)]
    fn call(data: &[u8]) -> Vec<u8> {
        // Here you could call some rather complex code that only compiles on native or
        // is way faster in native than executing it in wasm.
        [18].to_vec()
    }

    /// A function can take a `&self` or `&mut self` argument to get access to the
    /// `Externalities`. (The generated method does not require
    /// this argument, so the function can be called just with the `optional` argument)
    fn set_or_clear(&mut self, optional: Option<Vec<u8>>) {
        match optional {
            Some(value) => self.set_storage([1, 2, 3, 4].to_vec(), value),
            None => self.clear_storage(&[1, 2, 3, 4]),
        }
    }

    /// A function can be gated behind a configuration (`cfg`) attribute.
    /// To prevent ambiguity and confusion about what will be the final exposed host
    /// functions list, conditionally compiled functions can't be versioned.
    /// That is, conditionally compiled functions with `version`s greater than 1
    /// are not allowed.
    #[cfg(feature = "experimental-function")]
    fn gated_call(data: &[u8]) -> Vec<u8> {
        [42].to_vec()
    }
}

The given example will generate roughly the following code for native:

// The name of the trait is converted to snake case and used as mod name.
//
// Be aware that this module is not `public`, the visibility of the module is determined based
// on the visibility of the trait declaration.
mod interface {
    trait Interface {
        fn call_version_1(data: &[u8]) -> Vec<u8>;
        fn call_version_2(data: &[u8]) -> Vec<u8>;
        fn call_version_3(data: &[u8]) -> Vec<u8>;
        fn set_or_clear_version_1(&mut self, optional: Option<Vec<u8>>);
        #[cfg(feature = "experimental-function")]
        fn gated_call_version_1(data: &[u8]) -> Vec<u8>;
    }

    impl Interface for &mut dyn sp_externalities::Externalities {
        fn call_version_1(data: &[u8]) -> Vec<u8> { Vec::new() }
        fn call_version_2(data: &[u8]) -> Vec<u8> { [17].to_vec() }
        fn call_version_3(data: &[u8]) -> Vec<u8> { [18].to_vec() }
        fn set_or_clear_version_1(&mut self, optional: Option<Vec<u8>>) {
            match optional {
                Some(value) => self.set_storage([1, 2, 3, 4].to_vec(), value),
                None => self.clear_storage(&[1, 2, 3, 4]),
            }
        }
        #[cfg(feature = "experimental-function")]
        fn gated_call_version_1(data: &[u8]) -> Vec<u8> { [42].to_vec() }
    }

    pub fn call(data: &[u8]) -> Vec<u8> {
        // only latest version is exposed
        call_version_2(data)
    }

    fn call_version_1(data: &[u8]) -> Vec<u8> {
        <&mut dyn sp_externalities::Externalities as Interface>::call_version_1(data)
    }

    fn call_version_2(data: &[u8]) -> Vec<u8> {
        <&mut dyn sp_externalities::Externalities as Interface>::call_version_2(data)
    }

    fn call_version_3(data: &[u8]) -> Vec<u8> {
        <&mut dyn sp_externalities::Externalities as Interface>::call_version_3(data)
    }

    pub fn set_or_clear(optional: Option<Vec<u8>>) {
        set_or_clear_version_1(optional)
    }

    fn set_or_clear_version_1(optional: Option<Vec<u8>>) {
        sp_externalities::with_externalities(|mut ext| Interface::set_or_clear_version_1(&mut ext, optional))
            .expect("`set_or_clear` called outside of an Externalities-provided environment.")
    }

    #[cfg(feature = "experimental-function")]
    pub fn gated_call(data: &[u8]) -> Vec<u8> {
        gated_call_version_1(data)
    }

    #[cfg(feature = "experimental-function")]
    fn gated_call_version_1(data: &[u8]) -> Vec<u8> {
        <&mut dyn sp_externalities::Externalities as Interface>::gated_call_version_1(data)
    }

    /// This type implements the `HostFunctions` trait (from `sp-wasm-interface`) and
    /// provides the host implementation for the wasm side. The host implementation converts the
    /// arguments from wasm to native and calls the corresponding native function.
    ///
    /// This type needs to be passed to the wasm executor, so that the host functions will be
    /// registered in the executor.
    pub struct HostFunctions;
}

The given example will generate roughly the following code for wasm:

mod interface {
    mod extern_host_functions_impls {
        /// Every function is exported by the native code as `ext_FUNCTION_NAME_version_VERSION`.
        ///
        /// The type for each argument of the exported function depends on
        /// `<ARGUMENT_TYPE as RIType>::FFIType`.
        ///
        /// `key` holds the pointer and the length to the `data` slice.
        pub fn call(data: &[u8]) -> Vec<u8> {
            extern "C" { pub fn ext_call_version_2(key: u64); }
            // Should call into extenal `ext_call_version_2(<[u8] as IntoFFIValue>::into_ffi_value(key))`
            // But this is too much to replicate in a doc test so here we just return a dummy vector.
            // Note that we jump into the latest version not marked as `register_only` (i.e. version 2).
            Vec::new()
        }

        /// `key` holds the pointer and the length of the `option` value.
        pub fn set_or_clear(option: Option<Vec<u8>>) {
            extern "C" { pub fn ext_set_or_clear_version_1(key: u64); }
            // Same as above
        }

        /// `key` holds the pointer and the length to the `data` slice.
        #[cfg(feature = "experimental-function")]
        pub fn gated_call(data: &[u8]) -> Vec<u8> {
            extern "C" { pub fn ext_gated_call_version_1(key: u64); }
            /// Same as above
            Vec::new()
        }
    }

    /// The type is actually `ExchangeableFunction` (from `sp-runtime-interface`) and
    /// by default this is initialized to jump into the corresponding function in
    /// `extern_host_functions_impls`.
    ///
    /// This can be used to replace the implementation of the `call` function.
    /// Instead of calling into the host, the callee will automatically call the other
    /// implementation.
    ///
    /// To replace the implementation:
    ///
    /// `host_call.replace_implementation(some_other_impl)`
    pub static host_call: () = ();
    pub static host_set_or_clear: () = ();
    #[cfg(feature = "experimental-feature")]
    pub static gated_call: () = ();

    pub fn call(data: &[u8]) -> Vec<u8> {
        // This is the actual call: `host_call.get()(data)`
        //
        // But that does not work for several reasons in this example, so we just return an
        // empty vector.
        Vec::new()
    }

    pub fn set_or_clear(optional: Option<Vec<u8>>) {
        // Same as above
    }

    #[cfg(feature = "experimental-feature")]
    pub fn gated_call(data: &[u8]) -> Vec<u8> {
        // Same as above
        Vec::new()
    }
}

Argument types

The macro supports any kind of argument type, as long as it implements RIType and the required FromFFIValue/IntoFFIValue. The macro will convert each argument to the corresponding FFI representation and will call into the host using this FFI representation. On the host each argument is converted back to the native representation and the native implementation is called. Any return value is handled in the same way.

Wasm only interfaces

Some interfaces are only required from within the wasm runtime e.g. the allocator interface. To support this, the macro can be called like #[runtime_interface(wasm_only)]. This instructs the macro to make two significant changes to the generated code:

  1. The generated functions are not callable from the native side.
  2. The trait as shown above is not implemented for Externalities and is instead implemented for FunctionContext (from sp-wasm-interface).

Disable tracing

By adding no_tracing to the list of options you can prevent the wasm-side interface from generating the default sp-tracing-calls. Note that this is rarely needed but only meant for the case when that would create a circular dependency. You usually do not want to add this flag, as tracing doesn’t cost you anything by default anyways (it is added as a no-op) but is super useful for debugging later.