# 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;
}
```