Skip to content

Commit 378c902

Browse files
authored
Merge pull request #7625 from NlightNFotis/rust_api_crate_preparation
Rust crate preparation - final touches
2 parents c22c8a4 + 8ca2615 commit 378c902

File tree

9 files changed

+262
-136
lines changed

9 files changed

+262
-136
lines changed

.github/workflows/pull-request-check-rust-api.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ on:
77
env:
88
default_build_dir: "build/"
99
default_solver: "minisat2"
10+
default_include_dir: "src/libcprover-cpp/"
1011

1112
# For now, we support two jobs: A Linux and a MacOS based one.
1213
# Both of the jobs use CMake, as we only support building the Rust
@@ -64,7 +65,7 @@ jobs:
6465
VERSION=$(cat src/config.inc | python3 -c "import sys,re;line = [line for line in sys.stdin if re.search('CBMC_VERSION = (\d+\.\d+\.\d+)', line)];sys.stdout.write(re.search('CBMC_VERSION = (\d+\.\d+\.\d+)', line[0]).group(1))")
6566
cd src/libcprover-rust;\
6667
cargo clean;\
67-
CBMC_LIB_DIR=../../${{env.default_build_dir}}/lib CBMC_VERSION=$VERSION cargo test -- --test-threads=1
68+
CBMC_INCLUDE_DIR=../../${{env.default_include_dir}} CBMC_LIB_DIR=../../${{env.default_build_dir}}/lib CBMC_VERSION=$VERSION cargo test -- --test-threads=1
6869
6970
7071
check-macos-12-cmake-clang-rust:
@@ -104,4 +105,4 @@ jobs:
104105
VERSION=$(cat src/config.inc | python3 -c "import sys,re;line = [line for line in sys.stdin if re.search('CBMC_VERSION = (\d+\.\d+\.\d+)', line)];sys.stdout.write(re.search('CBMC_VERSION = (\d+\.\d+\.\d+)', line[0]).group(1))")
105106
cd src/libcprover-rust;\
106107
cargo clean;\
107-
CBMC_LIB_DIR=../../${{env.default_build_dir}}/lib CBMC_VERSION=$VERSION cargo test -- --test-threads=1
108+
CBMC_INCLUDE_DIR=../../${{env.default_include_dir}} CBMC_LIB_DIR=../../${{env.default_build_dir}}/lib CBMC_VERSION=$VERSION cargo test -- --test-threads=1

src/libcprover-rust/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ description = "Rust API for CBMC and assorted CProver tools"
66
repository = "https://github.com/diffblue/cbmc"
77
documentation = "https://diffblue.github.io/cbmc/"
88
license = "BSD-4-Clause"
9-
exclude = ["other/", "module_dependencies.txt"]
9+
exclude = ["module_dependencies.txt"]
1010

1111
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1212

src/libcprover-rust/build.rs

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use std::env;
22
use std::env::VarError;
33
use std::io::{Error, ErrorKind};
4-
use std::path::Path;
54
use std::path::PathBuf;
65

76
fn get_current_working_dir() -> std::io::Result<PathBuf> {
@@ -37,12 +36,40 @@ fn get_library_build_dir() -> std::io::Result<PathBuf> {
3736
))
3837
}
3938

39+
// This is needed so that cargo can find the include folder for the C++
40+
// API headers at compile time.
41+
fn get_include_directory_envvar() -> Result<String, VarError> {
42+
env::var("CBMC_INCLUDE_DIR")
43+
}
44+
45+
fn get_include_directory() -> std::io::Result<PathBuf> {
46+
let env_var_fetch_result = get_include_directory_envvar();
47+
if let Ok(build_dir) = env_var_fetch_result {
48+
let mut path = PathBuf::new();
49+
path.push(build_dir);
50+
return Ok(path);
51+
}
52+
Err(Error::new(
53+
ErrorKind::Other,
54+
"Environment variable `CBMC_INCLUDE_DIR' not set",
55+
))
56+
}
57+
4058
fn main() {
41-
let cbmc_source_path = Path::new("..");
42-
let cpp_api_path = Path::new("../libcprover-cpp/");
59+
let cpp_api_include_path = match get_include_directory() {
60+
Ok(path) => path,
61+
Err(err) => {
62+
let error_message = &format!(
63+
"Error: {}.\n Advice: {}.",
64+
err,
65+
"Please set the environment variable `CBMC_INCLUDE_DIR' with the path that contains cprover/api.h on your system"
66+
);
67+
panic!("{}", error_message);
68+
}
69+
};
70+
4371
let _build = cxx_build::bridge("src/lib.rs")
44-
.include(cbmc_source_path)
45-
.include(cpp_api_path)
72+
.include(cpp_api_include_path)
4673
.include(get_current_working_dir().unwrap())
4774
.file("src/c_api.cc")
4875
.flag_if_supported("-std=c++11")

src/libcprover-rust/include/c_api.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
#pragma once
44

5-
#include <util/exception_utils.h>
6-
75
#include <memory>
6+
#include <string>
87

98
// NOLINTNEXTLINE(build/include)
109
#include "rust/cxx.h"
10+
// NOLINTNEXTLINE(build/include)
11+
#include "include/c_errors.h"
1112

1213
struct api_sessiont;
1314

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Author: Fotis Koutoulakis for Diffblue Ltd, 2023.
2+
3+
#pragma once
4+
5+
// The following type is cloning two types from the `util/exception_utils.h` and
6+
// `util/invariant.h` files.
7+
//
8+
// The reason we need to do this is as follows: We have a fundamental constraint
9+
// in that we don't want to export internal headers to the clients, and our
10+
// current build system architecture on the C++ end doesn't allow us to do so.
11+
//
12+
// At the same time, we want to allow the Rust API to be able to catch at the
13+
// shimlevel the errors generated within CBMC, which are C++ types (and
14+
// subtypes of those), and so because of the mechanism that cxx.rs uses, we
15+
// need to have thetypes present at compilation time (an incomplete type won't
16+
// do - I've tried).
17+
//
18+
// This is the best way that we have currently to be have the type definitions
19+
// around so that the exception handling code knows what our exceptions look
20+
// like (especially given that they don't inherit from `std::exception`), so
21+
// that our system compiles and is functional, without needing include chains
22+
// outside of the API implementation (which we can't expose as well).
23+
24+
// This should mirror the definition in `util/invariant.h`.
25+
class invariant_failedt
26+
{
27+
private:
28+
const std::string file;
29+
const std::string function;
30+
const int line;
31+
const std::string backtrace;
32+
const std::string condition;
33+
const std::string reason;
34+
35+
public:
36+
virtual ~invariant_failedt() = default;
37+
38+
virtual std::string what() const noexcept;
39+
40+
invariant_failedt(
41+
const std::string &_file,
42+
const std::string &_function,
43+
int _line,
44+
const std::string &_backtrace,
45+
const std::string &_condition,
46+
const std::string &_reason)
47+
: file(_file),
48+
function(_function),
49+
line(_line),
50+
backtrace(_backtrace),
51+
condition(_condition),
52+
reason(_reason)
53+
{
54+
}
55+
};
56+
57+
// This is needed here because the original definition is in the file
58+
// <util/exception_utils.h> which is including <util/source_location.h>, which
59+
// being an `irep` is a no-go for our needs as we will need to expose internal
60+
// headers as well.
61+
class cprover_exception_baset
62+
{
63+
public:
64+
/// A human readable description of what went wrong.
65+
/// For readability, implementors should not add a leading
66+
/// or trailing newline to this description.
67+
virtual std::string what() const;
68+
virtual ~cprover_exception_baset() = default;
69+
70+
protected:
71+
/// This constructor is marked protected to ensure this class isn't used
72+
/// directly. Deriving classes should be used to more precisely describe the
73+
/// problem that occurred.
74+
explicit cprover_exception_baset(std::string reason)
75+
: reason(std::move(reason))
76+
{
77+
}
78+
79+
/// The reason this exception was generated. This is the string returned by
80+
/// `what()` unless that method is overridden
81+
std::string reason;
82+
};

src/libcprover-rust/readme.md

Lines changed: 118 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# CProver (CBMC) Rust API
1+
# Libcprover-rust
22

3-
This folder contains the implementation of the Rust API of the CProver (CBMC) project.
3+
A Rust interface for convenient interaction with the CProver tools.
44

55
## Building instructions
66

@@ -15,7 +15,9 @@ project:
1515

1616
* `CBMC_LIB_DIR`, for selecting where the `libcprover-x.y.z.a` is located
1717
(say, if you have downloaded a pre-packaged release which contains
18-
the static library), and
18+
the static library),
19+
* `CBMC_INCLUDE_DIR`, for selecting where the `cprover/api.h` is located,
20+
and
1921
* `CBMC_VERSION`, for selecting the version of the library to link against
2022
(this is useful if you have multiple versions of the library in the same
2123
location and you want to control which version you compile against).
@@ -27,7 +29,7 @@ directory of the CBMC project.)
2729
```sh
2830
$ cd src/libcprover-rust
2931
$ cargo clean
30-
$ CBMC_LIB_DIR=../../build/lib CBMC_VERSION=5.78.0 cargo build
32+
$ CBMC_INCLUDE_DIR=../../build/include CBMC_LIB_DIR=../../build/lib CBMC_VERSION=5.78.0 cargo build
3133
```
3234

3335
To build the project and run its associated tests, the command sequence would
@@ -36,7 +38,118 @@ look like this:
3638
```sh
3739
$ cd src/libcprover-rust
3840
$ cargo clean
39-
$ CBMC_LIB_DIR=../../build/lib CBMC_VERSION=5.78.0 cargo test -- --test-threads=1 --nocapture
41+
$ CBMC_INCLUDE_DIR=../../build/include CBMC_LIB_DIR=../../build/lib CBMC_VERSION=5.78.0 cargo test -- --test-threads=1 --nocapture
42+
```
43+
44+
## Basic Usage
45+
46+
This file will guide through a sample interaction with the API, under a basic
47+
scenario: *loading a file and verifying the model contained within*.
48+
49+
To begin, we will assume that you have a file under `/tmp/api_example.c`,
50+
with the following contents:
51+
52+
```c
53+
int main(int argc, char *argv[])
54+
{
55+
int arr[] = {0, 1, 2, 3};
56+
__CPROVER_assert(arr[3] != 3, "expected failure: arr[3] == 3");
57+
}
58+
```
59+
60+
The first thing we need to do to initiate any interaction with the API
61+
itself is to create a new `api_sessiont` handle by using the function
62+
`new_api_session`:
63+
64+
```rust
65+
let client = cprover_api::new_api_session();
66+
```
67+
68+
Then, we need to add the file to a vector with filenames that indicate
69+
which files we want the verification engine to load the models of:
70+
71+
```rust
72+
let vec: Vec<String> = vec!["/tmp/api_example.c".to_owned()];
73+
74+
let vect = ffi_util::translate_rust_vector_to_cpp(vec);
75+
```
76+
77+
In the above code example, we created a Rust language Vector of Strings
78+
(`vec`). In the next line, we called a utility function from the module
79+
`ffi_util` to translate the Rust `Vec<String>` into the C++ equivalent
80+
`std::vector<std::string>` - this step is essential, as we need to translate
81+
the type into something that the C++ end understands.
82+
83+
These operations are *explicit*: we have opted to force users to translate
84+
between types at the FFI level in order to reduce the "magic" and instill
85+
mental models more compatible with the nature of the language-border (FFI)
86+
work. If we didn't, and we assumed the labour of translating these types
87+
transparently at the API level, we risked mistakes from our end or from the
88+
user end frustrating debugging efforts.
89+
90+
At this point, we have a handle of a C++ vector containing the filenames
91+
of the files we want the CProver verification engine to load. To do so,
92+
we're going to use the following piece of code:
93+
94+
```rust
95+
// Invoke load_model_from_files and see if the model has been loaded.
96+
if let Err(_) = client.load_model_from_files(vect) {
97+
eprintln!("Failed to load model from files: {:?}", vect);
98+
process::exit(1);
99+
}
100+
```
101+
102+
The above is an example of a Rust idiom known as a `if let` - it's effectively
103+
a pattern match with just one pattern - we don't match any other case.
104+
105+
What we we do above is two-fold:
106+
107+
* We call the function `load_model_from_files` with the C++ vector (`vect`)
108+
we prepared before. It's worth noting that this function is being called
109+
with `client.` - what this does is that it passes the `api_session` handle
110+
we initialised at the beginning as the first argument to the `load_model_from_files`
111+
on the C++ API's end.
112+
* We handled the case where the model loading failed for whatever reason from
113+
the C++ end by catching the error on the Rust side and printing a suitable error
114+
message and exiting the process gracefully.
115+
116+
---
117+
118+
*Interlude*: **Error Handling**
119+
120+
`cxx.rs` (the FFI bridge we're using to build the Rust API) allows for a mechanism
121+
wherein exceptions from the C++ program can be translated into Rust `Result<>` types
122+
provided suitable infrastructure has been built.
123+
124+
Our Rust API contains a C++ shim which (among other things) intercepts CProver
125+
exceptions (from `cbmc`, etc.) and translates them into a form that the bridge
126+
can then translate to appropriate `Result` types that the Rust clients can use.
127+
128+
This means that, as above, we can use the same Rust idioms and types as we would
129+
use on a purely Rust based codebase to interact with the API.
130+
131+
*All of the API calls* are returning `Result` types such as above.
132+
133+
---
134+
135+
After we have loaded the model, we can proceed to then engage the verification
136+
engine for an analysis run:
137+
138+
```rust
139+
if let Err(_) = client.verify_model() {
140+
eprintln!("Failed to verify model from files: {:?}", vect);
141+
process::exit(1);
142+
}
143+
```
144+
145+
While all this is happening, we are collecting the output of the various
146+
phases into a message buffer. We can go forward and print any messages from
147+
that buffer into `stdout`:
148+
149+
```rust
150+
let msgs_cpp = cprover_api::get_messages();
151+
let msgs_rust = ffi_util::translate_cpp_vector_to_rust(msgs_cpp);
152+
ffi_util::print_response(msgs_rust);
40153
```
41154

42155
## Notes

src/libcprover-rust/src/c_api.cc

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@
33
// NOLINTNEXTLINE(build/include)
44
#include "include/c_api.h"
55

6-
#include <util/invariant.h>
7-
#include <util/make_unique.h>
8-
9-
#include <libcprover-cpp/api.h>
6+
// clang-format off
7+
#include <api.h>
8+
// clang-format on
109

1110
#include <algorithm>
1211
#include <cassert>
@@ -29,7 +28,8 @@ _translate_vector_of_string(rust::Vec<rust::String> elements)
2928
std::back_inserter(*stdv),
3029
[](rust::String elem) { return std::string(elem); });
3130

32-
POSTCONDITION(elements.size() == stdv->size());
31+
// NOLINTNEXTLINE(build/deprecated)
32+
assert(elements.size() == stdv->size());
3333
return *stdv;
3434
}
3535

src/libcprover-rust/src/lib.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
#![doc = include_str!("../tutorial.md")]
1+
#![doc = include_str!("../readme.md")]
22
#![warn(missing_docs)]
33

44
/// The main API module for interfacing with CProver tools (`cbmc`, `goto-analyzer`, etc).
55
#[cxx::bridge]
66
pub mod cprover_api {
77

88
unsafe extern "C++" {
9-
include!("libcprover-cpp/api.h");
9+
include!("api.h");
1010
include!("include/c_api.h");
1111

1212
/// Central organisational handle of the API. This directly corresponds to the
@@ -101,6 +101,22 @@ mod tests {
101101
assert_eq!(vect.len(), 2);
102102
}
103103

104+
// This test will capture a `system_exceptiont` from CBMC's end at the C++ shim that this
105+
// library depends on, and it will be correctly translated into the Result type for Rust.
106+
// This also validates that our type definition include of the base class for the exceptions
107+
// works as we expect it to.
108+
#[test]
109+
fn it_translates_exceptions_to_errors() {
110+
let client = cprover_api::new_api_session();
111+
112+
// The vector of string is supposed to contain a string denoting
113+
// a filepath that is erroneous.
114+
let vec: Vec<String> = vec!["/fkjsdlkjfisudifoj2309".to_owned()];
115+
let vect = ffi_util::translate_rust_vector_to_cpp(vec);
116+
117+
assert!(client.load_model_from_files(vect).is_err());
118+
}
119+
104120
#[test]
105121
fn it_can_load_model_from_file() {
106122
let binding = cprover_api::new_api_session();

0 commit comments

Comments
 (0)