How to reproduce C-style external function calls in Rust?

Hello, I'm a junior dev moving from C to Rust. I'm currently trying to port a library from C to Rust and I'm struggling to reproduce a specific behavior.

In C, I had the following:

// my_lib.h
typedef struct {
    uint8_t i2c_address;
} my_struct_t;

uint8_t send_to_i2c(my_struct_t *s);

// my_lib/some_file.c
void some_func(my_struct_t *s) {
    send_to_i2c(s);
}

In this setup, the library user had to implement the send_to_i2c function. I can't figure out how to achieve this in Rust. How can my library know that this function exists in a safe way while leaving the implementation to the client? Are the Trait the solution?

Thanks for you time!
Ben

Yes. Traits allow you to describe the interface that your library expects, leaving the implementation up to the caller.

1 Like

If you need actual C-style, including compatibility with C libraries, and the attached potential unsafety pitfalls from C (especially if you get the signatures mismatching) Rust does have extern blocks (Unsafe Rust - The Rust Programming Language) (External blocks - The Rust Reference).

From rust code to rust code, you wouldn't necessarily use any of those. Instead, trait based solutions would be used: those would generally involve adding a generic argument to your API such as

fn some_func<S: SendToI2c>(s: &MyStruct) {
   S::send_to_i2c(s);
}

this means that different downstream users can provide different send_to_i2c implementation without conflict. Rust does not (yet?) support, at least natively, any way of allowing for similarly downstream provided functionality while restricting to only one implementation globally.

If you go this way, the trait could be defined like

pub trait SendToI2c {
    fn send_to_i2c(&MyStruct);
}
pub struct MyStruct {
    i2c_address: u8,
} 

and if the whole library depends on several such functions at once, you can bundle them up into a single trait if you like (feel free to choose a more fitting name in that case).

The downstream user than implements the trait for some marker type (common choices might be a empty unit struct, or maybe even an empty enum)

struct SendI2cImpl;

or

enum SendI2ClcImpl {} 

(feel free to choose a mor descriptive name relating more to what your implementation actually does, not just what trait it implements) and then an implementation

impl upstream_crate::SendToI2c for SendI2cImpl {
    fn send_to_i2c(&upstream_crate::MyStruct) {
        // code goes here
    } 
} 

then the code can call your API such as this:

fn foo() {
    let xyz: MyStruct = //...

    some_func::<SendI2cImpl>(&xyz);
}

For more ad-hoc use cases when only a single function is involved, directly expecting some impl Fn(&MyStruct) argument passed to some_func may also be a possible alternative API design choice.

If the choice of implementation of the interface is logically coupled to the lifetime of a specific instance of the MyStruct value (in particular if you're not supposed to switch out that send_to_i2c logic between multiple calls to some_func on the same MyStruct, then it might also make sense to psrametrize the MyStruct type with your trait, like

struct MyStuct<S: SendToI2c> {
    i2c_address: u8,
    _marker: PhantomData<fn(S) -> S>, // trick for phantom marker invariant in `S`
} 

// I'm skipping the details of adjusting the definitions of the trait and your function signature
// since it's a lot to type on mobile rn ;-) 

This way, users done have to manually specify their parameter on every call to some_func anymore, but only once when initializing the struct, and then type inference can help afterwards.

It's hard to give more concrete advice without a better feel of the overall shape of the API surface though.

3 Likes