One of the first things I learned when programming professionally is that global variables are bad. We all take it for granted that it’s bad practice to write code that relies heavily on global state but the other day I was working with a 3rd party native library, and it reminded why these best practices come about.
There are a couple factors which made this particular library’s use of global mutable state rather ugly to work with,
- Thread safety - mutating something from two concurrent threads of execution without proper synchronisation is a data race, a common form of Undefined Behaviour. This means the library can only ever be used from one place at a time.
- Brittle code - Relying on global state often means the code expects to be run in a very specific order. Failing to invoke functions in the correct order can lead to memory issues (e.g. leaks or double-frees) and accidentally corrupting the library’s state
- Poor testability - when code is mutating global variables there’s no way to
inject mocks (e.g. to
assert!()
specific conditions) or make sure individual chunks of functionality work. You are often reduced to writing one or two high-level integration tests which execute the “happy path”, and when an error occurs it’s hard to narrow it down to just one function
To be clear, when I refer to “global state” I am referring to variables in
a program (typically declared with the keyword static
in languages like C#,
Java, and Rust) which live for the lifetime of the program, only ever have
one instance, and where all uses of the variable are hard-coded to use that
instance (i.e. code refers to the variable by name instead of via a reference).
These variables don’t necessarily need to be publicly accessible. All of these problems occur when using static local variables inside a function or class. The core problems related to having static lifetime and code directly referencing the variable are still there.
For my use case it’s not really viable to use a different library because the alternatives don’t necessarily have the features we need. We also can’t rewrite the code to not use global mutable state because it’s closed-source, and even then would require tens of developer-years of effort. Which is tens of developer-years more than I have to spare.
This necessitates a more pragmatic solution. In short, we needed to find a way to use this 3rd party library without it polluting the rest of the application too much.
I’ll be implementing this in Rust primarily because it’s quite good at enforcing guarantees via the type system, but the general ideas aren’t really language-specific. It may just require more runtime checks.
The code written in this article is available on GitHub. Feel free to browse through and steal code or inspiration.
If you found this useful or spotted a bug, let me know on the blog’s issue tracker!
Getting Acquainted Link to heading
It would be a bit overwhelming to show you the full code, so I’ve prepared a rough example we can play around with.
Another important point to emphasize is the 3rd party library which initially inspired this experiment is native code (I think it’s written in C++). The reason this is important is because it works touches memory directly and a bug doesn’t just mean an exception gets thrown, we could segfault and tear down the entire process.
My decision to write use it from Rust also means we’ll need to write some
unsafe
code when crossing the language boundary and enforcing invariants.
This is actually a good thing! It gives us several easy-to-find places to
start from when troubleshooting a crash.
Without further ado, here’s a header file defining the high-level interface of the library we’ll be working with.
// native/stateful.h
#if !defined(STATEFUL_H)
#define STATEFUL_H
#include <stdbool.h>
#ifdef __cplusplus
extern "C"
{
#endif
// The various possible return codes.
enum
{
// The function completed successfully.
RESULT_OK,
// A function was called out of order.
RESULT_BAD_STATE,
// One of the provided arguments is invalid.
RESULT_INVALID_ARGUMENT,
};
// Initialize the library. MUST be run before any other function.
int stateful_open();
// Clean up any state associated with this library.
int stateful_close();
// Begin setting parameters. MUST be run before any parameters can be set.
int stateful_start_setting_parameters();
int stateful_set_bool_var(const char *name, bool value);
int stateful_set_int_var(const char *name, int value);
// Finish setting parameters.
int stateful_end_setting_parameters();
// Start adding input items.
int stateful_start_adding_items();
// Add a single item as an input.
int stateful_add_item(const char *name, int value);
// Start adding a group of items.
int stateful_start_adding_group(const char *name);
// Add an item to the current group. stateful_start_adding_group MUST be called
// beforehand.
int stateful_add_group_item(const char *name, int value);
// Finish adding items to the current group, adding the overall group to the
// list of inputs.
int stateful_end_adding_group();
// Finish setting up the list of inputs.
int stateful_end_adding_items();
// A callback used to notify the caller when progress is made.
typedef int (*progress_cb)(int percent);
// A callback used to let the user retrieve results.
typedef int (*result_cb)(int number_of_results);
// Run the code.
int stateful_execute(progress_cb progress, result_cb result);
// Try to get the number of outputs in the result.
int stateful_get_num_outputs(int *value);
// Tries to retrieve a particular output.
int stateful_get_output_by_index(int index, int *value);
#ifdef __cplusplus
}
#endif
#endif // STATEFUL_H
The difficulty with trying to explain complex architecture is to find the sweet spot between simplifying so much that people miss the point or the example feels contrived, and providing so much detail that the reader loses track of what’s going on amongst the various moving parts.
Unfortunately, that means our stateful.h
needs to be more than a couple
functions long to do this topic justice.
There’s quite a lot going on here, so let’s unpack it a bit.
Fortunately, this library does a pretty decent job of handling errors and most of the time functions will give you some sort of return code.
// The various possible return codes.
enum
{
// The function completed successfully.
RESULT_OK,
// A function was called out of order.
RESULT_BAD_STATE,
// One of the provided arguments is invalid.
RESULT_INVALID_ARGUMENT,
};
In reality there are going to be a lot more error cases, but you get the gist.
The library needs to initialize some global state before first use and do
cleanup at the end, so our stateful
library has open()
and close()
functions.
// Initialize the library. MUST be run before any other function.
int stateful_open();
// Clean up any state associated with this library.
int stateful_close();
(I don’t know about you, but usually when I see this sort of code I’ll start thinking of using RAII)
After initializing the library we need to set some global parameters. These are various knobs and levers that are used to alter how the input is processed.
// Begin setting parameters. MUST be run before any parameters can be set.
int stateful_start_setting_parameters();
int stateful_set_bool_var(const char *name, bool value);
int stateful_set_int_var(const char *name, int value);
// Finish setting parameters.
int stateful_end_setting_parameters();
You can see that we need to explicitly start and stop setting parameters. The functions themselves take no arguments, which is a big give-away that the code mutates global state under the hood.
Next we’ve got functions for setting up the input.
// Start adding input items.
int stateful_start_adding_items();
// Add a single item as an input.
int stateful_add_item(const char *name, int value);
// Start adding a group of items.
int stateful_start_adding_group(const char *name);
// Add an item to the current group. stateful_start_adding_group MUST be called
// beforehand.
int stateful_add_group_item(const char *name, int value);
// Finish adding items to the current group, adding the overall group to the
// list of inputs.
int stateful_end_adding_group();
// Finish setting up the list of inputs.
int stateful_end_adding_items();
If you squint, you’ll see that the input is a list of individual named items
or groups of items. The Input
that we’re building procedurally might look
something like this (if written in Rust):
enum ItemValue {
Single(i32),
Group(HashMap<String, i32>),
}
struct Item {
name: String,
value: ItemValue,
}
type Inputs = Vec<Item>;
/// The global we're initializing between `stateful_start_adding_items()` and
/// `stateful_send_adding_items()`.
static mut TEMP_INPUTS: Inputs = ...;
Now we’ve set the algorithm’s parameters and created our input, we can execute the code.
// A callback used to notify the caller when progress is made.
typedef int (*progress_cb)(int percent);
// A callback used to let the user retrieve results.
typedef int (*result_cb)(int number_of_results);
// Run the code.
int stateful_execute(progress_cb progress, result_cb result);
You’ll notice this uses callback functions to notify the caller of progress and
when the results are ready. This wouldn’t normally be a problem, except the
code doesn’t let us provide some sort of void *
pointer to user-provided data.
That means the only way our callbacks will be able to pass information to the
caller is by itself using static variables.
Finally, we get a couple functions for inspecting the output.
// Try to get the number of outputs in the result.
int stateful_get_num_outputs(int *value);
// Tries to retrieve a particular output.
int stateful_get_output_by_index(int index, int *value);
Something to keep in mind is they can only be called from inside our
result_cb
callback. Other than that, the functions are pretty ordinary.
I’m going to be deliberately vague about what stateful_execute()
actually
does. For our purposes the computation isn’t actually relevant, we’re mainly
concerned about how you can make use of such a “stateful” library while
maintaining nice things like,
- thread-safety
- memory-safety
- statically ensuring at compile time that it is impossible to do things out of order
If it helps, think of stateful_execute()
as something like this:
fn stateful_execute(
parameters: &HashMap<String, Value>,
items: Vec<Input>,
on_progress: impl FnMut(percent: i32),
) -> Vec<i32> {
// magic
}
We take a set of parameters and list of inputs and return a list of integers.
Our High-Level Approach Link to heading
We have two main goals for this exercise,
- Create a safe wrapper which lets us use this library while maintaining memory and thread-safety
- Use the type system to make illegal states unrepresentable
The first goal can be fulfilled fairly easily, because this library can only be
used by one bit of code at a time (static
variables aren’t thread-safe) we
can make a type which represents a “handle” to the library.
We can then write our code in such a way that calling a function from our
stateful
library needs you to have a valid handle.
// src/lib.rs
use std::marker::PhantomData;
/// A handle to the `stateful` library.
pub struct Library {
_not_send: PhantomData<*const ()>,
}
Something to note is the use of PhantomData<*const ()>
here. This makes sure
Library
is !Send
and !Sync
(i.e. it can’t be used from another thread).
We can double-check that Library
can’t be used from other threads by adding
the static_assertions
crate as a dev-dependency…
$ cargo add --dev static_assertions
Updating 'https://github.com/rust-lang/crates.io-index' index
Adding static_assertions v1.1.0 to dev-dependencies
… And then writing a new test.
// src/lib.rs
#[cfg(test)]
mod tests {
use super::*;
static_assertions::assert_not_impl_any!(Library: Send, Sync);
}
If we add an impl for Send
, running cargo test
will show a build failure.
/// A handle to the `stateful` library.
pub struct Library {
_not_send: PhantomData<*const ()>,
}
+unsafe impl Send for Library {}
The error message leaves a lot to be desired, but this is what you’d see if
Library
were Send
.
$ cargo test
Compiling stateful-native-library v0.1.0 (/home/michael/Documents/stateful-native-library)
error[E0282]: type annotations needed for `fn() {<Library as tests::_::{{closure}}#0::AmbiguousIfImpl<_>>::some_item}`
--> src/lib.rs:14:5
|
14 | static_assertions::assert_not_impl_any!(Library: Send, Sync);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| |
| consider giving this pattern the explicit type `fn() {<Library as tests::_::{{closure}}#0::AmbiguousIfImpl<_>>::some_item}`, with the type parameters specified
| cannot infer type
|
= note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)
error: aborting due to previous error
Now we have a thread-safe Library
type, we also need to make sure it’s not
possible to create more than one Library
at a time. This can be done easily
enough using a flag (AtomicBool
) which is set to true
when Library
is
created and false
when it is destroyed.
// src/lib.rs
use sync::atomic::{AtomicBool, Ordering};
static LIBRARY_IN_USE: AtomicBool = AtomicBool::new(false);
impl Library {
pub fn new() -> Result<Library, Error> {
if LIBRARY_IN_USE.compare_and_swap(false, true, Ordering::SeqCst)
== false
{
Ok(Library { _not_send: PhantomData })
} else {
Err(Error::AlreadyInUse)
}
}
}
impl Drop for Library {
fn drop(&mut self) { LIBRARY_IN_USE.store(false, Ordering::SeqCst); }
}
/// The various error cases that may be encountered while using this library.
#[derive(Debug, Copy, Clone, PartialEq, thiserror::Error)]
pub enum Error {
#[error("The library is already in use")]
AlreadyInUse,
}
Yes I know the irony in using a static
variable to workaround another
library’s zealous use of static
variables, but sometimes you’ve got to
break a couple eggs to make an omelette 🤷
To make sure we’ve implemented this correctly, let’s write a test which
deliberately tries to create multiple Library
handles at the same time.
// lib/src.rs
#[cfg(test)]
mod tests {
use super::*;
...
#[test]
fn cant_create_multiple_library_handles_at_the_same_time() {
let first_library = Library::new().unwrap();
// make sure the flag is set
assert!(LIBRARY_IN_USE.load(Ordering::SeqCst));
// then try to create another handle
assert!(Library::new().is_err());
// explicitly drop the first library so we know it clears the flag
drop(first_library);
assert!(!LIBRARY_IN_USE.load(Ordering::SeqCst));
// now the old handle is destroyed, we can create another
let _another = Library::new().unwrap();
}
}
Next, to ensure functions aren’t called out of order we can create some sort of type-level state machine.
This idea was originally taken from a thread on the Rust users forum. You’ll notice the approach we’re taking is uncannily similar to the solution proposed by @Yandros,
I would wrap the library using a type-level state machine to make misusage simply not compile; If necessary, you can even use the singleton pattern to enforce no concurrency problems (which would be the only part checked at runtime).
One of the invariants we’d like to maintain is that when setting parameters it should be impossible to use any non-parameter-setting functionality.
This is where lifetimes really show their power. Using a &mut Library
reference, the compiler can statically ensure some SettingParameters
type
(which we’re about to create) has unique access to our Library
.
// src/lib.rs
impl Library {
...
pub fn set_parameters(&mut self) -> SettingParameters<'_> {
SettingParameters { _library: self }
}
}
pub struct SettingParameters<'lib> {
_library: &'lib mut Library,
}
impl<'lib> SettingParameters<'lib> {
pub fn boolean(&mut self, _name: &str, _value: bool) -> &mut Self {
unimplemented!()
}
pub fn integer(&mut self, _name: &str, _value: i32) -> &mut Self {
unimplemented!()
}
}
You’ll notice I’m deliberately leaving the body for boolean()
and
integer()
as unimplemented!()
. We’re just setting up the infrastructure
for this “type-level state machine” for now and will execute the actual FFI
calls in a bit.
Once we’ve set the various parameters we need to start constructing our inputs.
This can be done using some sort of RecipeBuilder
which leverages the
&mut Library
trick from SettingParameters
.
// src/lib.rs
impl Library {
...
pub fn create_recipe(&mut self) -> RecipeBuilder<'_> {
RecipeBuilder { _library: self }
}
}
pub struct RecipeBuilder<'lib> {
_library: &'lib mut Library,
}
impl<'lib> RecipeBuilder<'lib> {
pub fn add_item(&mut self, _name: &str, _value: i32) -> &mut Self {
unimplemented!()
}
}
Now we’ve got a problem, how can you add a group of items to the “recipe”? We
need to make sure it’s not possible to call RecipeBuilder::add_item()
while
in the middle of constructing a group because it could mess up the library’s
internals.
This may seem a bit extreme, but it’s quite possible that adding a new item
triggers a resize in our TEMP_INPUTS
vector which invalidates any references
that may have been used when constructing the group.
I really don’t want something like this to happen because it’s one of those
bugs that will be almost impossible to pinpoint in the wild. It’s better to
take the conservative approach here and just outlaw things like
RecipeBuilder::add_item()
when building a group.
If you think for a moment, that’s the same problem we had with Library
when
we wanted to make sure you can either be creating a recipe using the
RecipeBuilder
, or setting parameters with SettingParameters
. We just need to
add another layer of &mut
s!
// src/lib.rs
impl<'lib> RecipeBuilder<'lib> {
...
pub fn add_group<'r>(&'r mut self, _name: &str) -> GroupBuilder<'r, 'lib> {
GroupBuilder {
_recipe_builder: self,
}
}
}
pub struct GroupBuilder<'r, 'lib> {
_recipe_builder: &'r mut RecipeBuilder<'lib>,
}
impl<'r, 'lib> GroupBuilder<'r, 'lib> {
pub fn add_item(&mut self, _name: &str, _value: i32) -> &mut Self { unimplemented!() }
pub fn finish(self) -> &'r mut RecipeBuilder<'lib> { self._recipe_builder }
}
Next, I’m going to continue with this builder pattern theme and give
RecipeBuilder
a build()
method which returns a Recipe
. Here our
Recipe
will just be an empty type indicating we’ve fully assembled the
inputs.
It acts as a “token” indicating all inputs are constructed and we’re ready to
call stateful_execute()
.
// src/lib.rs
impl<'lib> RecipeBuilder<'lib> {
...
pub fn build(self) -> Recipe<'lib> {
Recipe {
_library: self._library,
}
}
}
pub struct Recipe<'lib> {
_library: &'lib mut Library,
}
If you’ve been keeping up, we’re now at the point where everything is
initialized and we’re ready to consume this Recipe
to get the output.
It seems odd for a Recipe
to know how to execute itself, so we’ll create
some sort of top-level execute()
function instead of adding it as a method.
// src/lib.rs
pub fn execute<P>(_recipe: Recipe<'_>, _progress: P) -> Result<Output, Error>
where
P: FnMut(i32),
{
unimplemented!()
}
pub struct Output {
pub items: Vec<i32>,
}
To make sure the code we’ve written prevents invalid uses of Library
, let’s
use Rustdoc’s compile_fail
feature to document our functions with examples
we expect rustc
to reject.
// src/lib.rs
impl Library {
...
/// Start creating the inputs for [`execute()`].
///
/// The [`RecipeBuilder`] uses lifetimes to make sure you can't do anything
/// else while creating a [`Recipe`].
///
/// ```rust,compile_fail
/// # use stateful_native_library::Library;
/// let mut library = Library::new().unwrap();
///
/// // start creating the recipe
/// let mut recipe_builder = library.create_recipe();
///
/// // trying to set parameters while recipe_builder is alive is an error
/// library.set_parameters();
///
/// // recipe_builder is still alive until here
/// drop(recipe_builder);
/// ```
pub fn create_recipe(&mut self) -> RecipeBuilder<'_> {
...
}
}
Looking back, you can see that fulfilling the !Send
and !Sync
goal was
quite easy to do. By making sure Library: !Send + !Sync
, anything
referencing Library
is also !Send + !Sync
.
On the other hand, ensuring functions can only be called in the correct order is a lot more invasive. We needed to restructure our entire API using complex concepts like lifetimes and RAII to encode the logical equivalent of a type-level state machine.
Here’s a more detailed example showing the error messages a user would get if they tried to do things in the wrong order.
#[test]
fn this_should_not_compile() {
let mut library = Library::new().unwrap();
{
let _sp = library.set_parameters();
library.create_recipe(); // ERROR
}
let recipe = {
let mut recipe_builder = library.create_recipe();
let group_builder = recipe_builder.add_group("Group");
// try to add an item to the recipe while building a group
recipe_builder.add_item("asdf", 123); // ERROR
// finish building the group
group_builder.finish();
// we can now add items
recipe_builder.add_item("asdf", 123);
// you can't set parameters while building the recipe
library.set_parameters(); // ERROR
recipe_builder.build()
};
let got = execute(recipe, |percent| println!("Progress: {}%", percent));
assert!(got.is_ok());
}
And this is what rustc
will emit:
$ cargo test
Compiling stateful-native-library v0.1.0 (/home/michael/Documents/stateful-native-library)
error[E0499]: cannot borrow `library` as mutable more than once at a time
--> src/lib.rs:284:13
|
283 | let _sp = library.set_parameters();
| ------- first mutable borrow occurs here
284 | library.create_recipe(); // ERROR
| ^^^^^^^ second mutable borrow occurs here
285 | }
| - first borrow might be used here, when `_sp` is dropped and runs the `Drop` code for type `SettingParameters`
error[E0499]: cannot borrow `recipe_builder` as mutable more than once at a time
--> src/lib.rs:292:13
|
289 | let group_builder = recipe_builder.add_group("Group");
| -------------- first mutable borrow occurs here
...
292 | recipe_builder.add_item("asdf", 123); // ERROR
| ^^^^^^^^^^^^^^ second mutable borrow occurs here
...
295 | group_builder.finish();
| ------------- first borrow later used here
error[E0499]: cannot borrow `library` as mutable more than once at a time
--> src/lib.rs:301:13
|
288 | let mut recipe_builder = library.create_recipe();
| ------- first mutable borrow occurs here
...
301 | library.set_parameters(); // ERROR
| ^^^^^^^ second mutable borrow occurs here
302 |
303 | recipe_builder.build()
| -------------- first borrow later used here
error: aborting due to 3 previous errors
For more information about this error, try `rustc --explain E0499`.
error: could not compile `stateful-native-library`.
To learn more, run the command again with --verbose.
It’s moments like these that make you appreciate how useful the concept of
lifetimes can be, and just how readable rustc
’s error messages are 🙂
Creating FFI Bindings Link to heading
While you were reading through that previous section I took the liberty of
preparing some C++ code which satisfies the stateful.h
interface.
The code isn’t entirely relevant, but it’s all on GitHub if you’re curious. I don’t write C++ full time, so let me know if you spot any bugs or code which could have been written better.
a big wall of code
// native/stateful.cpp
#include "stateful.h"
#include <vector>
#include <unordered_map>
#include <string>
#include <variant>
#include <stdint.h>
#include <memory>
enum class State
{
Uninitialized,
Initialized,
SettingParameters,
AddingInputs,
AddingGroup,
Executing,
};
class Item
{
public:
virtual void flatten(std::vector<int32_t> &dest) = 0;
virtual ~Item() {}
};
class Group : public Item
{
private:
std::unordered_map<std::string, int32_t> items;
public:
void insert(std::string key, int32_t value)
{
items.insert(std::make_pair(key, value));
}
void flatten(std::vector<int32_t> &dest)
{
for (auto &&pair : items)
{
dest.push_back(pair.second);
}
}
};
class SingleItem : public Item
{
private:
int32_t value;
public:
SingleItem(int32_t v) : value(v) {}
void flatten(std::vector<int32_t> &dest)
{
dest.push_back(value);
}
};
using Parameter = std::variant<int32_t, bool>;
// our actual global variables
State current_state = State::Uninitialized;
std::unordered_map<std::string, std::unique_ptr<Item>> *inputs;
std::unordered_map<std::string, Parameter> *parameters;
std::vector<int32_t> *temp_results;
std::string *temp_group_name;
Group *temp_group;
int stateful_open()
{
if (current_state != State::Uninitialized)
{
return RESULT_BAD_STATE;
}
inputs = new std::unordered_map<std::string, std::unique_ptr<Item>>();
parameters = new std::unordered_map<std::string, Parameter>();
current_state = State::Initialized;
return RESULT_OK;
}
int stateful_close()
{
if (inputs)
{
delete inputs;
inputs = nullptr;
}
if (parameters)
{
delete parameters;
parameters = nullptr;
}
current_state = State::Uninitialized;
return RESULT_OK;
}
int stateful_start_setting_parameters()
{
if (current_state != State::Initialized)
{
return RESULT_BAD_STATE;
}
current_state = State::SettingParameters;
return RESULT_OK;
}
template <typename T>
static int set_parameter(const char *name, T value)
{
if (current_state != State::SettingParameters)
{
return RESULT_BAD_STATE;
}
if (!name)
{
return RESULT_INVALID_ARGUMENT;
}
parameters->insert(std::make_pair(name, value));
return RESULT_OK;
}
int stateful_set_bool_var(const char *name, bool value)
{
return set_parameter(name, value);
}
int stateful_set_int_var(const char *name, int value)
{
return set_parameter(name, value);
}
int stateful_end_setting_parameters()
{
if (current_state != State::SettingParameters)
{
return RESULT_BAD_STATE;
}
current_state = State::Initialized;
return RESULT_OK;
}
int stateful_start_adding_items()
{
if (current_state != State::Initialized)
{
return RESULT_BAD_STATE;
}
current_state = State::AddingInputs;
return RESULT_OK;
}
template <typename T>
int add_input(std::string name, const T value)
{
if (current_state != State::AddingInputs)
{
return RESULT_BAD_STATE;
}
inputs->insert(std::pair(name, std::make_unique<T>(value)));
return RESULT_OK;
}
int stateful_add_item(const char *name, int value)
{
return add_input(name, SingleItem(value));
}
int stateful_start_adding_group(const char *name)
{
if (current_state != State::AddingInputs)
{
return RESULT_BAD_STATE;
}
temp_group_name = new std::string(name);
temp_group = new Group();
current_state = State::AddingGroup;
return RESULT_OK;
}
int stateful_add_group_item(const char *name, int value)
{
if (current_state != State::AddingGroup)
{
return RESULT_BAD_STATE;
}
temp_group->insert(name, value);
return RESULT_OK;
}
int stateful_end_adding_group()
{
if (current_state != State::AddingGroup)
{
return RESULT_BAD_STATE;
}
current_state = State::AddingInputs;
add_input(*temp_group_name, *temp_group);
delete temp_group;
temp_group = nullptr;
delete temp_group_name;
temp_group_name = nullptr;
return RESULT_OK;
}
int stateful_end_adding_items()
{
if (current_state != State::AddingInputs)
{
return RESULT_BAD_STATE;
}
current_state = State::Initialized;
return RESULT_OK;
}
// This overload stuff legitimately feels like magic...
// https://www.bfilipek.com/2018/06/variant.html#visitors-for-stdvariant
template <class... Ts>
struct overload : Ts...
{
using Ts::operator()...;
};
template <class... Ts>
overload(Ts...)->overload<Ts...>;
int stateful_execute(progress_cb progress, result_cb result)
{
if (current_state != State::Initialized)
{
return RESULT_BAD_STATE;
}
current_state = State::Executing;
std::vector<int32_t> results;
int i = 0;
for (auto &pair : *inputs)
{
pair.second->flatten(results);
double percent = 100.0 * i / inputs->size();
progress((int)percent);
i++;
}
progress(100);
temp_results = &results;
result(results.size());
temp_results = nullptr;
current_state = State::Initialized;
return RESULT_OK;
}
int stateful_get_num_outputs(int *value)
{
if (current_state != State::Executing)
{
return RESULT_BAD_STATE;
}
*value = temp_results->size();
return RESULT_OK;
}
int stateful_get_output_by_index(int index, int *value)
{
if (current_state != State::Executing)
{
return RESULT_BAD_STATE;
}
auto &results = *temp_results;
if (index >= results.size())
{
return RESULT_INVALID_ARGUMENT;
}
*value = results[index];
return RESULT_OK;
}
The first step in using our stateful library from Rust is to make sure cargo
automatically compiles the code for us. Luckily the cc
crate was
designed for exactly this purpose.
First we need to add cc
as a build
dependency.
$ cargo add --build cc
Updating 'https://github.com/rust-lang/crates.io-index' index
Adding cc v1.0.50 to build-dependencies
Then we can create a build script.
// build.rs
use cc::Build;
use std::{env, path::PathBuf};
fn main() {
let project_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let native = project_dir.join("native");
Build::new()
.include(&native)
.file(native.join("stateful.cpp"))
.cpp(true)
.flag_if_supported("-std=c++17")
.compile("stateful");
}
Next we need declarations that for the functions in stateful.h
that can be
called from Rust. These sorts of things are a pain to do by hand, so I almost
always use bindgen
for this.
I ended up using this particular set of incantations:
$ bindgen native/stateful.h \
--whitelist-function 'stateful_.*' \
--whitelist-var 'RESULT_.*' \
--output src/bindings.rs \
--raw-line '#![allow(bad_style, dead_code)]'
$ head src/bindings.rs
/* automatically generated by rust-bindgen */
#![allow(bad_style, dead_code)]
extern "C" {
pub fn stateful_open() -> ::std::os::raw::c_int;
}
extern "C" {
pub fn stateful_close() -> ::std::os::raw::c_int;
}
We also need to update lib.rs
to include src/bindings.rs
as a sub-module.
// src/lib.rs
mod bindings;
Before going any further I’d like to double-check the stateful
native library
is linked with our Rust code properly. The easiest way to do that is with a
simple smoke test.
// src/lib.rs
#[cfg(test)]
mod tests {
use super::*;
use std::os::raw::c_int;
...
#[test]
fn ffi_bindings_smoke_test() {
unsafe {
assert_eq!(bindings::stateful_open(), bindings::RESULT_OK as c_int);
assert_eq!(bindings::stateful_close(), bindings::RESULT_OK as c_int);
}
}
}
It’s always a good idea to do these sorts of sanity checks. For one, you’ve added a test that verifies you can always link with the native library, plus it also lets you identify other build problems early on.
Ironically, while writing that smoke test and saying how important it is to
have them, I kept having compilation errors because the linker wasn’t able to
find stateful_open()
and stateful_close()
.
It turns out I forgot to add a #ifdef __cplusplus extern "C" {
line to
stateful.h
to prevent the compiler from mangling the symbols for our
stateful_*
. It took a couple minutes of staring at linker errors, but after
compiling stateful.cpp
manually and using nm stateful.o | grep ' T '
I
noticed the mangled names and figured out what was going on.
Writing a Safe Interface to libstateful Link to heading
We’re now ready to go from declaring our type-level state machine to giving it some behaviour to execute when transitioning from state to state.
To make things easier we’re going to define a helper trait for converting from
a return code to a Result<(), Error>
.
// src/lib.rs
use std::convert::TryFrom;
trait IntoResult {
fn into_result(self) -> Result<(), Error>;
}
impl IntoResult for c_int {
fn into_result(self) -> Result<(), Error> {
let code = u32::try_from(self).map_err(|_| Error::Other(self))?;
match code {
bindings::RESULT_OK => Ok(()),
bindings::RESULT_BAD_STATE => Err(Error::InvalidState),
bindings::RESULT_INVALID_ARGUMENT => Err(Error::InvalidArgument),
_ => Err(Error::Other(self)),
}
}
}
By itself this trait isn’t overly interesting, but we can leverage it to enable
?
for error handling.
This also gives us a chance to flesh out the Error
enum.
// src/lib.rs
#[derive(Debug, Copy, Clone, PartialEq, thiserror::Error)]
pub enum Error {
#[error("The library is already in use")]
AlreadyInUse,
#[error("The underlying library is in an invalid state")]
InvalidState,
#[error("An argument was invalid")]
InvalidArgument,
#[error("Unknown error code: {}", _0)]
Other(c_int),
}
Proper RAII Link to heading
Now that’s out of the way, the first part to address is our Library
type.
At the moment we’re just setting a flag to true
and returning a Library
handle, but we aren’t actually initializing the underlying library.
// src/lib.rs
impl Library {
pub fn new() -> Result<Library, Error> {
if LIBRARY_IN_USE.compare_and_swap(false, true, Ordering::SeqCst)
== false
{
+ unsafe {
+ bindings::stateful_open().into_result()?;
+ }
+
Ok(Library { _not_send: PhantomData })
} else {
Err(Error::AlreadyInUse)
}
}
You can also see how that IntoResult
trait helps to remove the visual noise
associated with constant error checks.
When the Library
gets destroyed we need to make sure everything gets cleaned
up.
// src/lib.rs
impl Drop for Library {
- fn drop(&mut self) { LIBRARY_IN_USE.store(false, Ordering::SeqCst); }
+ fn drop(&mut self) {
+ unsafe {
+ let _ = bindings::stateful_close().into_result();
+ }
+ LIBRARY_IN_USE.store(false, Ordering::SeqCst);
+ }
}
The ordering of operations is important here. We want to make sure the
LIBRARY_IN_USE
flag is set to true
for the entire time we’re interacting
with the underlying code.
Our SettingParameters
and RecipeBuilder
types use RAII to represent when
the library is in a certain state. We’ll need to call the corresponding
*_start_*
and *_end_*
functions when constructing and destroying them to
make sure their lifetimes align with the state of the native library.
// src/lib.rs
impl Library {
...
pub fn set_parameters(&mut self) -> SettingParameters<'_> {
+ cant_fail!(unsafe { bindings::stateful_start_setting_parameters() });
SettingParameters { _library: self }
}
pub fn create_recipe(&mut self) -> RecipeBuilder<'_> {
+ cant_fail!(unsafe { bindings::stateful_start_adding_items() });
RecipeBuilder {
_library: ManuallyDrop::new(self),
}
}
}
...
+impl<'lib> Drop for SettingParameters<'lib> {
+ fn drop(&mut self) {
+ unsafe {
+ let _ = bindings::stateful_end_setting_parameters().into_result();
+ }
+ }
+}
...
+impl<'lib> Drop for RecipeBuilder<'lib> {
+ fn drop(&mut self) {
+ unsafe {
+ let _ = bindings::stateful_end_adding_items().into_result();
+ }
+ }
+}
You’ll notice that I’ve introduced a cant_fail!()
macro here. I’ve
introduced an assertion which will blow up loudly if that assumption isn’t
valid. Because we’re using the type system to statically guarantee code can’t
be executed out of order and that the arguments we provide are always valid,
these otherwise infallible functions should fail quickly and loudly to
indicate a possible programming error.
// src/lib.rs
/// A macro you can use when you *know* a function has been statically proven to
/// not fail.
macro_rules! cant_fail {
($return_code:expr) => {
if let Err(e) = $return_code.into_result() {
unreachable!(
"The type system should ensure this function can't fail: {}",
e
);
}
};
}
We need to do a bit more work when adding calls to
stateful_start_adding_group()
and stateful_end_adding_group()
for our
GroupBuilder
because it is the first proper function that we need to
provide arguments for.
To help convert between a Rust &str
and const char *
we can use the
std::ffi::CString
type. The constructor can fail with a NulError
if a string contains an internal null
byte, but considering most “proper”
strings in Rust won’t ever contain null
it seems fair to simplify the API by
panicking instead of propagating the error.
// src/lib.rs
const NUL_MSG: &str = "a valid Rust string shouldn't contain null characters";
impl<'lib> RecipeBuilder<'lib> {
...
pub fn add_group<'r>(&'r mut self, name: &str) -> GroupBuilder<'r, 'lib> {
let name = CString::new(name).expect(NUL_MSG);
cant_fail!(unsafe {
bindings::stateful_start_adding_group(name.as_ptr())
});
GroupBuilder { _recipe_builder: ManuallyDrop::new(self) }
}
}
impl<'r, 'lib> Drop for GroupBuilder<'r, 'lib> {
fn drop(&mut self) {
unsafe {
let _ = bindings::stateful_end_adding_group().into_result();
}
}
}
Finishing It Off Link to heading
At this point it’s just a case of grepping for any remaining functions
containing unimplemented!()
and translating the arguments so they can be
passed to the corresponding functions in our stateful
library.
First up are the methods on SettingParameters
.
// src/lib.rs
impl<'lib> SettingParameters<'lib> {
pub fn boolean(&mut self, name: &str, value: bool) -> &mut Self {
let name = CString::new(name).expect(NUL_MSG);
unsafe {
cant_fail!(bindings::stateful_set_bool_var(name.as_ptr(), value));
}
self
}
pub fn integer(&mut self, name: &str, value: i32) -> &mut Self {
let name = CString::new(name).expect(NUL_MSG);
unsafe {
cant_fail!(bindings::stateful_set_int_var(name.as_ptr(), value));
}
self
}
}
Then RecipeBuilder
.
// src/lib.rs
impl<'lib> RecipeBuilder<'lib> {
pub fn add_item(&mut self, name: &str, value: i32) -> &mut Self {
let name = CString::new(name).expect(NUL_MSG);
cant_fail!(unsafe {
bindings::stateful_add_item(name.as_ptr(), value)
});
self
}
...
}
Our GroupBuilder
also has an unimplemented!()
method that needs
implementing.
// src/lib.rs
impl<'r, 'lib> GroupBuilder<'r, 'lib> {
pub fn add_item(&mut self, name: &str, value: i32) -> &mut Self {
let name = CString::new(name).expect(NUL_MSG);
cant_fail!(unsafe {
bindings::stateful_add_group_item(name.as_ptr(), value)
});
self
}
...
}
The only remaining unimplemented!()
is in our execute()
function.
This one is a little tricky because we somehow need to pass results from the
“finished” callback back to our execute()
function so they can be returned to
the caller.
Unfortunately, we don’t have the option of passing a pointer to some state
we can put the result in, so we’ll need to stash it in a temporary static
variable.
If you are ever designing a C API, the correct way to implement non-trivial
callbacks is by accepting a void *
user data pointer.
I would highly recommend checking out this question on StackOverflow for more!
Because this function is pretty long, I’m going to break it up into a couple chunks.
First we have our temporary variables. This is a place to store our temporary
result and a pointer to the data attached to the progress
closure.
// src/lib.rs
pub fn execute<P>(_recipe: Recipe<'_>, mut progress: P) -> Result<Output, Error>
where
P: FnMut(i32),
{
// Safety: Accepting a `Recipe` means we prove at compile time that setting
// these variables can't result in any data races.
static mut ON_PROGRESS_USER_DATA: *mut c_void = ptr::null_mut();
static mut TEMPORARY_RESULT: Option<Output> = None;
...
}
Next we’ve got a definition for the actual on_progress()
function we’ll be
passing to stateful_execute()
. This uses a trick where you instantiate the
generic on_progress<F>()
function with a particular type, turning it into a
non-generic function which is specialised for the closure type passed to
execute()
.
// src/lib.rs
pub fn execute<P>(_recipe: Recipe<'_>, mut progress: P) -> Result<Output, Error>
where
P: FnMut(i32),
{
...
unsafe extern "C" fn on_progress<F>(percent: c_int) -> c_int
where
F: FnMut(i32),
{
// Safety: This requires us to store a pointer to `progress` when
// `execute()` is called and make sure the `F` type variable
// `on_progress()` is instantiated with is the same as `execute()`'s `P`
let actual_progress_callback = &mut *(ON_PROGRESS_USER_DATA as *mut F);
actual_progress_callback(percent);
bindings::RESULT_OK as c_int
}
...
}
If that explanation makes your head hurt a little, just hang in there, it should get a bit clearer once we see how it gets used.
Next we define an on_finished()
function to pass to stateful_execute()
. This
keeps reading outputs until there are no more, and saves them to the
TEMPORARY_RESULT
static variable.
// src/lib.rs
pub fn execute<P>(_recipe: Recipe<'_>, mut progress: P) -> Result<Output, Error>
where
P: FnMut(i32),
{
...
unsafe extern "C" fn on_finished(_num_items: c_int) -> c_int {
let mut output = Output::default();
let mut item = 0;
while bindings::stateful_get_output_by_index(
output.items.len() as c_int,
&mut item,
) == bindings::RESULT_OK as c_int
{
output.items.push(item);
}
// Safety: Accepting a `Recipe` means this can only be set by one thread
// at a time
TEMPORARY_RESULT = Some(output);
bindings::RESULT_OK as c_int
}
...
}
Finally we can actually call stateful_execute()
and process the results.
// src/lib.rs
pub fn execute<P>(_recipe: Recipe<'_>, mut progress: P) -> Result<Output, Error>
where
P: FnMut(i32),
{
...
unsafe {
ON_PROGRESS_USER_DATA = &mut progress as *mut P as *mut c_void;
let ret = bindings::stateful_execute(
Some(on_progress::<P>),
Some(on_finished),
)
.into_result();
// We need to take the temporary result before handling
// stateful_execute()'s return code so we don't leak an `Output`.
let output = TEMPORARY_RESULT.take();
// just bail if something went wrong
if let Err(e) = ret {
return Err(e);
}
// We need to make sure we actually set the temporary result. The only
// way this could possibly happen is if `stateful_execute()` ran to
// completion and said it finished successfully without actually
// invoking our `on_finished` callback... If so, that's a programming
// error in the underlying library and nothing the caller can reasonably
// be expected to handle
match output {
Some(output) => Ok(output),
None => panic!("The stateful_execute function said it returned
successfully without calling our on_finished callback. This is a
bug.")
}
}
}
I’m not going to explain this in much detail because it should hopefully be fairly readable and well commented. Plus, reading the source code is always a more accurate explanation of what is going on than several paragraphs of English.
The most important thing to note is the liberal sprinkling of comments
starting with // Safety
. These are hints to other developers (or myself 6
months from now) about the various invariants which need to be upheld, and the
reason I believe this unsafe
code is sound.
This execute()
function contains about 80 lines of unsafe
code. If you
see something that looks a bit funny, please let me know either in the
stateful-native-library
repository or this blog’s issue
tracker.
Writing correct code is very important to me, especially when it is being read or used by others!
Making Sure It Works Link to heading
Now we’ve gone to all the effort of binding to our stateful
native library,
let’s write a small program which uses it.
To that end, I’ve created the following contrived example,
// examples/basic-usage.rs
//! A contrived example showing how you can use the `stateful_native_library`
//! bindings.
use stateful_native_library::{Error, Library};
fn main() -> Result<(), Error> {
let mut library = Library::new()?;
// set some parameters
library
.set_parameters()
.boolean("foo", false)
.integer("some-number", 42);
// start building the recipe
let mut recipe_builder = library.create_recipe();
recipe_builder.add_item("first", 1).add_item("second", 2);
// we can add several groups using a loop
for i in 0..5 {
let name = format!("group_{}", i);
let mut group_builder = recipe_builder.add_group(&name);
for j in 0..i {
let name = format!("group_{}_item_{}", i, j);
group_builder.add_item(&name, i + j);
}
group_builder.finish();
}
// or use normal builder methods
recipe_builder
.add_group("another group")
.add_item("another nested item", 5)
.add_item("MOAR items", 6);
// finish building the recipe
let recipe = recipe_builder.build();
// then get the outcome, periodically printing out progress messages
let outcome = stateful_native_library::execute(recipe, |percent| {
println!("{}%", percent)
})?;
println!("Got {:?}", outcome);
Ok(())
}
And this is what you get when you run it:
$ cargo run --example basic-usage
Compiling stateful-native-library v0.1.0 (/home/michael/Documents/stateful-native-library)
Finished dev [unoptimized + debuginfo] target(s) in 4.72s
Running `target/debug/examples/basic-usage`
0%
12%
25%
37%
50%
62%
75%
87%
100%
Got Output { items: [6, 4, 7, 5, 5, 3, 4, 2, 6, 5, 1, 1, 3, 2] }
Running the basic-usage
example compiled with optimisations (which I would
expect to be more likely to show UB) under valgrind
seems to show no obvious
problems.
$ valgrind target/release/examples/basic-usage
==9668== Memcheck, a memory error detector
==9668== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==9668== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==9668== Command: target/release/examples/basic-usage
==9668==
0%
12%
25%
37%
50%
62%
75%
87%
100%
Got Output { items: [6, 4, 7, 5, 5, 3, 4, 2, 6, 5, 1, 1, 3, 2] }
==9668==
==9668== HEAP SUMMARY:
==9668== in use at exit: 0 bytes in 0 blocks
==9668== total heap usage: 160 allocs, 160 frees, 81,197 bytes allocated
==9668==
==9668== All heap blocks were freed -- no leaks are possible
==9668==
==9668== For lists of detected and suppressed errors, rerun with: -s
==9668== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
Conclusions Link to heading
In an ideal world all code would be perfect and we’d never need to worry about data races or accidentally putting a system in an invalid state.
Unfortunately, this isn’t an ideal world so we need to develop techniques that allow us to keep working under less than ideal conditions while limiting the negative effects they can have on the rest of a codebase.
As it turns out, Rust’s type system and its concept of lifetimes are really powerful tools for taking errors that other languages could only detect at runtime and promoting them to compilation failures. That’s one of the things I really like about the language, more often than not if it compiles, it works.