# Native Client API - Instantiate on the application side, using library functions - Get opaque pointers and use them to call mutating library functions on those opaque structures - Only pass pointers and serializable configuration data across the boundary ## Path #1: Wrapping the whole client Things to consider: - We should allow app-defined resolvers and plugins. I think it'd probably go like this: 1. We define a general-use header file that defines the shape of a PluginWrapper in general: ```hpp! #ifndef PLUGIN_WRAPPER_HPP #define PLUGIN_WRAPPER_HPP #include <cstdint> class PluginWrapper { public: PluginWrapper(); virtual ~PluginWrapper(); virtual int invoke(const uint8_t* data, size_t size) = 0; }; #endif ``` 2. Import the header definitions into the Rust FFI lib: ```rust! use std::os::raw::{c_char, c_uint}; use libc::size_t; use crate::plugin_wrapper::PluginWrapper; #[no_mangle] pub extern "C" fn invoke_plugin(s: *mut PluginWrapper, data: *const u8, size: size_t) -> c_uint { if s.is_null() { return 1; } let plugin_wrapper = unsafe { &mut *s }; let result = plugin_wrapper.invoke(unsafe { std::slice::from_raw_parts(data, size) }); result as c_uint } ``` 3. Define a plugin class on the app side, and import the header file as well, and override the PluginWrapper class: ```cpp! #include "plugin_wrapper.hpp" #include "ffi_native_lib.h" // Header file generated by Rust FFI class MyPlugin : public PluginWrapper { public: virtual int invoke(const uint8_t* data, size_t size) override { ... } }; ``` 4. Pass down the derived class instance to the rust library and invoke it when appropriate in Rust Some implementation pseudo examples: - Config Builder Example (pseudo): ```cpp const char* builder_ptr = ffi_create_client_builder() ffi_add_resolver(&builder_ptr, resolver1) ffi_add_resolver(&builder_ptr, resolver2) ffi_add_resolver(&builder_ptr, resolver3) const char* native_client = build_client(&builder_ptr); ``` - Build an app side struct to wrap native_client function invocations: ```cpp! NativeClient::NativeClient() { client_ = ffi_create_client(); // Call the FFI function to create the RustClient struct } NativeClient::~NativeClient() { ffi_destroy_client(client_); // Call the FFI function to destroy the RustClient struct } void NativeClient::invoke(const uint8_t* data, size_t size) { ffi_invoke(client_, data, size); // Call the FFI function to invoke the Rust function } ``` And the usage would roughly look like: ```cpp! int main() { NativeClient client; uint8_t data[] = {...msgpack data blabla }; size_t size = sizeof(data) / sizeof(data[0]); client.invoke(data, size); return 0; } ``` ## Path 2: Only wrapping Wasm runtime layer + utils ## Useful resources on known obstacles: - https://adventures.michaelfbryan.com/posts/ffi-safe-polymorphism-in-rust/ # Simple Example Project 1. native DLL w/ stateful store 2. foreign binding interface (swift <> dll) 3. swift app w/ object instance ```swift= // our app w/ stateful custom objects class Animal { virtual speak(); } class Cow { speak() { console.log("moo") } } // native->swift function bindings function animal_speak(instancePtr: uint32) { const instance = reinterpret_cast<Animal>(instancePtr) instance.speak(); } class Farm { private _nativeLib; private _farmPtr; constructor() { this._farmPtr = this._nativeLib.create_farm(); } addAnimal(instance: Animal) { const animalPtr = ptr_cast(instance); const animalFuncs: IAnimalCApi = { animal_speak }; this._nativeLib.add_animal( this._farmPtr, animalPtr, animalFuncs ); } } function main_demo() { const farm = new Farm(); farm.addAnimal( new Cow() ); farm.speak("cow"); farm.speak("pig"); // Ex: swift -> native obj const cow = farm.getAnimal("cow"); cow.speak(); } ``` ```cpp void add_animal(...) { // PATTERN = passing pointers that reinterpret cast INTO stateful objects } void create_farm(): u32 { const farm = new Farm(); // NEED: add some default animals farm.add(new Pig()); return &farm; } ```