Background
Rust generic is a very powerful concept. With a well-encapsulated library, the down-stream users can really achieve "write one code, fit into multiple situations" without any runtime overhead.
For example:
trait Common {
type Field;
}
struct Comm1;
impl Common for Comm1 {
type Field = u64;
}
struct Comm2;
impl Common for Comm2 {
type Field = u32;
}
struct Foo<C: Common> {
field: C::Field,
}
fn util<C: Common>(foo: &Foo<C>) { /* ... */ }
Then the down-stream users can manipulate both Foo<Comm1>
and Foo<Comm2>
with util
and more functions generic over C: Common
:
fn my_downstream_impl<C: Common>(foo: &Foo<C>) {
let field = &foo.field;
util(foo);
// ...
}
Problem
What is the best practice to expose those generics to C/Python/... via FFI?
In a simplest solution, we can just define extern FFI functions:
#[no_mangle]
extern "C" fn util_comm1(foo: *const Foo<Comm1>) { /* ... */ }
#[no_mangle]
extern "C" fn util_comm2(foo: *const Foo<Comm2>) { /* ... */ }
Then the C/Python side could just use those functions.
However, in this solution, C/Python side could not achieve the "write one code, fit into multiple situations" goal. They must write:
void my_downstream_impl_comm1(void *foo) {
util_comm1(foo);
}
void my_downstream_impl_comm2(void *foo) {
util_comm2(foo);
}
To workaround this problem, C could use macros:
// comm1.h
#define FIELD uint64_t
#define util util_comm1
// comm2.h
#define FIELD uint32_t
#define util util_comm2
// rust-ffi.h
struct Foo {
FIELD field;
}
void util(struct Foo *foo);
// downstream.c
#include "comm1.h"
#include "rust-ffi.h"
void my_downstream_impl(struct Foo *foo) {
FIELD field = foo->field;
util(foo);
}
To make this code fit into both Comm1
and Comm2
, the above code should be compiled twice into two binaries, with different headers included.
I don't know if this approach is the best practice to handle this problem, and I really want to know how to do this right and best in C/Python FFI situations.