CBMC
|
A Rust interface for convenient interaction with the CProver tools.
To build the Rust project you need the Rust language toolchain installed (you can install from rustup.rs).
With that instaled, you can execute cargo build
under this (src/libcprover-rust
) directory.
For this to work, you need to supply two environment variables to the project:
CBMC_LIB_DIR
, for selecting where the libcprover-x.y.z.a
is located (say, if you have downloaded a pre-packaged release which contains the static library),CBMC_INCLUDE_DIR
, for selecting where the cprover/api.h
is located, andCBMC_VERSION
, for selecting the version of the library to link against (this is useful if you have multiple versions of the library in the same location and you want to control which version you compile against).As an example, a command sequence to build the API through cargo
would look like this (assuming you're executing these instructions from the root level directory of the CBMC project.)
To build the project and run its associated tests, the command sequence would look like this:
This file will guide through a sample interaction with the API, under a basic scenario: loading a file and verifying the model contained within.
To begin, we will assume that you have a file under /tmp/api_example.c
, with the following contents:
The first thing we need to do to initiate any interaction with the API itself is to create a new api_sessiont
handle by using the function new_api_session
:
Then, we need to add the file to a vector with filenames that indicate which files we want the verification engine to load the models of:
In the above code example, we created a Rust language Vector of Strings (vec
). In the next line, we called a utility function from the module ffi_util
to translate the Rust Vec<String>
into the C++ equivalent std::vector<std::string>
- this step is essential, as we need to translate the type into something that the C++ end understands.
These operations are explicit: we have opted to force users to translate between types at the FFI level in order to reduce the "magic" and instill mental models more compatible with the nature of the language-border (FFI) work. If we didn't, and we assumed the labour of translating these types transparently at the API level, we risked mistakes from our end or from the user end frustrating debugging efforts.
At this point, we have a handle of a C++ vector containing the filenames of the files we want the CProver verification engine to load. To do so, we're going to use the following piece of code:
The above is an example of a Rust idiom known as a if let
- it's effectively a pattern match with just one pattern - we don't match any other case.
What we we do above is two-fold:
load_model_from_files
with the C++ vector (vect
) we prepared before. It's worth noting that this function is being called with client.
- what this does is that it passes the api_session
handle we initialised at the beginning as the first argument to the load_model_from_files
on the C++ API's end.Interlude: Error Handling
cxx.rs
(the FFI bridge we're using to build the Rust API) allows for a mechanism wherein exceptions from the C++ program can be translated into Rust Result<>
types provided suitable infrastructure has been built.
Our Rust API contains a C++ shim which (among other things) intercepts CProver exceptions (from cbmc
, etc.) and translates them into a form that the bridge can then translate to appropriate Result
types that the Rust clients can use.
This means that, as above, we can use the same Rust idioms and types as we would use on a purely Rust based codebase to interact with the API.
All of the API calls are returning Result
types such as above.
After we have loaded the model, we can proceed to then engage the verification engine for an analysis run:
While all this is happening, we are collecting the output of the various phases into a message buffer. We can go forward and print any messages from that buffer into stdout
:
ffi
module within lib.rs
.Result
type.