How difficult is it to write a safe wrapper, C FFI

Hi, I'm considering writing a safe wrapper for GitHub - open62541/open62541: Open source implementation of OPC UA (OPC Unified Architecture) aka IEC 62541 licensed under Mozilla Public License v2.0 for a project at work.

Reading blog posts etc. I get an impression that it's almost a minefield of lifetimes, memory leaks, and undefined behaviour. I'm a bit worried about the difficulty of writing a safe wrapper and that it might be problems in production.

How difficult is it really, do I need to be very experienced? I know the basics of FFI, I'm more worried about what I don't know.

Thanks for any help with this not so concrete question :slight_smile:

1 Like

Well, it depends on whether the library has any complicated APIs.

People (especially in the Rust community) make Undefined Behaviour out to be the boogeyman, but if this is just a small project with limited scope then the worst that can happen is your program crashes or it is subtly broken. It sucks when it happens, but those sorts of bugs are incredibly valuable for honing your intuition and debugging skills!

I've found that a good exercise is to write a version of the upstream library's test suite using your Rust wrapper and then run the test binary under something like valgrind to detect UB and memory leaks. That will give you some confidence in your bindings and help to unravel lots of bugs.

Some questions to ask yourself while you are designing your bindings:

  • What's the ownership story here? So things like which components own each other, when I'm passing a pointer to a C function are we letting it borrow the value (&T or &mut T) or are we passing ownership (Box<T>)
  • How will my Rust crate compile and link to the C library? Will this work outside of my dev machine?
  • When one C object has a pointer into another, do I want to represent this relationship in the Rust wrappers using lifetimes (like git2 does) or do I want to use reference-counted XXXInner objects (like ssh2)? Personally, I find the lifetimes version considerably less ergonomic because lifetimes are viral and end users often get sent down the self-referential struct path

I would still recommend giving it a go, but try to pitch it as an experiment to see whether something is feasible given your current skills rather than a project you will deliver by a certain deadline. That way there's less pressure to succeed and you are setting expectations for your colleagues.

4 Likes

I have use for OPC UA in Rust, so I'd be happy to see someone work on this.

Start by working on a -sys crate [which means you don't need to worry too much about ergonomics] and get the most basic functions you need wrapped, then take some simple client and server example, and figure out how you'd like to express it in Rust (functions vs methods, structs, traits, enums, etc). Then start prototyping the safe and ergonomic crate on top of the -sys crate and try to get the examples to run (successfully) using your API.

Try not to focus on potential obstacles until you know they are actual issues.

From what little I know about OPC UA, it was created when a whole industry suddenly realized that basing an industry standard on DCOM is completely insane. As such, I can't help think that it may involve an event loop and a bunch of callback functions. Not necessarily a problem, but if you want async this may require some more planning.

1 Like

How about : GitHub - locka99/opcua: A client and server implementation of the OPC UA specification written in Rust ?

1 Like

It's not as complete as the open62541 project. I'm unable to judge how much the missing features matter in practice though.

Would you say that wrapping a C library is at least as safe as doing the whole thing in C, or does Rust FFI bring a whole set of additional problems compared to a pure C solution?

I wouldn't be too concerned about Rust adding extra problems due to its constraints.

In practice, a lot of C libraries are fairly easy to wrap in Rust because the way you design C code is similar to the way you design Rust code. Both languages force the author to think about lifetimes and ownership (even if C doesn't use that terminology) because they don't use a GC, which means you tend to get similar structures (layering and trees of ownership instead of complex webs of objects, etc.). I guess you could call it convergent evolution.

There's a bit more cognitive overhead than a Rust developer is used to because a lot of things are implicit or not enforced by the type system in C - just look at all the answers to The multiple meanings of T fn(T*, T*) in C(++)! C programmers also tend to have more of a "she'll be right" attitude when it comes to footguns and writing sound APIs than Rust programers[1], but that mostly means you'll be relying on Rust's type system a bit more to ensure things can't be used incorrectly.

TBH, you can get 90% of the way there by just making sure methods take &self (think "shared reference", not "immutable") and types are !Send + !Sync (thread-safety is the exception in C, not the rule).

That said... I've been doing this for a while now and am probably aflicted with the Curse of Knowledge, so take everything I say with a pinch of salt.


  1. lol, sweeping generalisation there. Although it's what I've seen based on my experience both professionally and when C/C++ programmers start learning Rust and come to these forums. ↩︎

3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.