How to Set up a Minimal Backend
How to create a backend is quite language dependent, and what is easier in one may be harder in another. Below we will start setting up a simple backend. We'll show you how to set up a test in diplomat so you can start generating code quickly. Then we give you a template for a simple dynamic library that you can then link to your host language. Finally we provide a suggested checklist for your backend. It is not automatically generated so when in doubt look at diplomat's HIR
- project structure: You will need need to test your generated code so you should first set up a host language project. It should have all dependencies to be able to interface with native code (or WASM).
Setting up Basic Code Generation in a Test
Your backend should iterate over all TypeDefs
and generate the required code for these. To do that we start with an
ast::File
, which can
then be parsed into a Env
using the
all_types
method. Then we can create the
TypeContext
which is generated using the
from_ast
method. You will also need an
AttributeValidator
,
but should probably start with the simple
BasicAttributeValidator
.
We will now build an example by way of a test. A good starting point is to create a test for
generating a simple opaque struct without any methods. Your backend should go in the tool crate:
create a module tool/src/{backend}/mod.rs
(make sure you add a line pub mod backend;
to
tool/src/lib.rs
). Add the following to it
use diplomat_core::hir::{OpaqueDef, TypeContext, TypeId};
fn gen_opaque_def(ctx: &TypeContext, type_id: TypeId, opaque_path: &OpaqueDef) -> String {
"We'll get to it".into()
}
#[cfg(test)]
mod test {
use diplomat_core::{
ast::{self},
hir::{self, TypeDef},
};
use quote::quote;
#[test]
fn test_opaque_gen() {
let tokens = quote! {
#[diplomat::bridge]
mod ffi {
#[diplomat::opaque]
struct OpaqueStruct;
}
};
let item = syn::parse2::<syn::File>(tokens).expect("failed to parse item ");
let diplomat_file = ast::File::from(&item);
let env = diplomat_file.all_types();
let attr_validator = hir::BasicAttributeValidator::new("my_backend_test");
let context = match hir::TypeContext::from_ast(&env, attr_validator) {
Ok(context) => context,
Err(e) => {
for (_cx, err) in e {
eprintln!("Lowering error: {}", err);
}
panic!("Failed to create context")
}
};
let (type_id, opaque_def) = match context
.all_types()
.next()
.expect("Failed to generate first opaque def")
{
(type_id, TypeDef::Opaque(opaque_def)) => (type_id, opaque_def),
_ => panic!("Failed to find opaque type from AST"),
};
let generated = super::gen_opaque_def(&context, type_id, opaque_def);
insta::assert_snapshot!(generated)
}
}
You can now run
cargo test -p diplomat-tool -- backend::test --nocapture
You should also have a generated snapshot diplomat_tool__backend__test__opaque_gen.snap.new
which you can use to pick up your generated code.
How to Generate the Library
Now to actually test native methods you will need to create some kind of library, be it static, dynamic, or even WASM. In the following we will be creating a dynamically linked library.
You should set up a separate rust project next to your diplomat fork e.g. mybackendtest
cargo new --lib mybackendtest
with the following Cargo.toml
[package]
name = "mybackendtest"
version = "0.1.0"
edition = "2021"
[lib]
crate_type = ["cdylib"]
name = "mybackendtest"
[dependencies]
diplomat = {path = "../diplomat/macro"}
diplomat-runtime = {path = "../diplomat/runtime"}
Because you are using path dependencies, it is important that your library project be in the same directory as your fork of diplomat
Copy the following into your lib.rs
#[diplomat::bridge]
mod ffi {
#[diplomat::opaque]
struct OpaqueStruct;
impl OpaqueStruct {
pub fn add_two(i: i32) -> i32 {
i + 2
}
}
}
Note it is very important that the method be marked pub
otherwise diplomat will ignore it.
Now you can run
cargo build
to create a debug artifact in target/debug/libmybackendtest.dylib
Getting Access to your Native Method
Now we can add code that will iterate over all of the methods of the opaque struct.
First, copy the impl block for OpaqueStruct
into the test code underneath the OpaqueStruct
.
Next, update your the code for gen_opaque_def
to the following which will generate the native
symbol for your new impl method:
#![allow(unused)] fn main() { use crate::c2::CFormatter; fn gen_opaque_def(ctx: &TypeContext, type_id: TypeId, opaque_path: &OpaqueDef) -> String { let c_formatter = CFormatter::new(ctx); opaque_def .methods .iter() .map(|method| c_formatter.fmt_method_name(type_id, method)) .collect::<Vec<_>>() .join("\n") } }
Now your snapshot should have the following contents
---
source: tool/src/backend/mod.rs
assertion_line: 67
expression: generated
---
OpaqueStruct_add_two
where OpaqueStruct_add_two
is the native symbol for your method. It has a simple signature i32 -> i32
,
so now you have a dynamic library and a symbol to load from it that you can start building. Now it is up
to you to figure how to integrate these into your host language project skeleton.
Minimal Backend
You should now work on building a minimal backend that can generate opaque type definitions with methods that only accept and return primitive types.
You will need to update tool/src/lib.rs
to add handling for your backend.
Once you have the basics of a backend you can add attribute handling. The best way to do this is to check the existing backends e.g. dart(Note: git permalink may be out of date). The most important is to ignore disabled types and methods, as then you can take advantage of diplomat's feature tests and start building progressively.
Feature Tests
Diplomat already includes feature tests that you can disable with #[diplomat::attrs(disable, {backend})]
.
where {backend}
refers to your new backend. As you add functionality to your backend you
can progressively enable the types and methods for your backend. This way you can iterate with
working examples. These are called via cargo-make
e.g
cargo make gen-dart-feature
You can look at Makefile.toml
to see how tasks are defined. Most of the generative tasks make use of this
duckscript function
(Duckscript is a simple scripting language)
Backend Checklist
- primitive types: This will be the most basic piece of the backend, and you will want to implement them early in order to test your ability to correctly call methods.
-
opaque types:
- basic definiton
-
return a boxed opaque. This needs to be cleaned in managed languages.
You can use the autogenerated
{OpaqueName}_destroy({OpaqueName}*)
native method to clean up the memory of the associated opaque. - as self parameter
- as another parameter
- structs
- enums
- writeable
-
slices
- primitive slices
- str slices
- owned slices
- slices of strings
- strings
-
borrows. This is probably one of the trickiest things, as you need to ensure that managed objects don't get
cleaned up if something depends on them.
- borrows of parameters
- in struct fields
- nullables, i.e. returning option types.
- fallibles, i.e. returning result types. The resulting native type will be a discriminated union.