Skip to content

feat: Expose pact_matching and pact_models over FFI. #97

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 37 commits into from
Apr 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
c2c64d2
Added empty pact_matching_ffi crate to workspace
alilleybrinker Apr 15, 2020
eac17cf
Added error handling / reporting, and other basic utilities. (#8)
alilleybrinker Apr 16, 2020
b57a970
Establish pact_matching_ffi logging mechanism
alilleybrinker Apr 16, 2020
6405181
Added basic FFI for Pact Messages
alilleybrinker Apr 17, 2020
7d6c101
Add cmake and an example (#23)
alilleybrinker Jun 22, 2020
a35e5d3
Add additional FFI Message APIs
cstepanian Jun 24, 2020
af31f17
Bump pact_matching version in FFI (#24)
alilleybrinker Jun 25, 2020
f6cddcc
Configured CMake to run Doxygen on FFI header file (#25)
alilleybrinker Jun 26, 2020
44700c8
Message style improvements (#26) (#27)
alilleybrinker Jul 30, 2020
7a339fa
Added metadata key/value iteration (#28)
alilleybrinker Aug 6, 2020
a99c7b0
Add support for reading Message provider states (#29)
alilleybrinker Sep 2, 2020
4510dbb
Adding README.md with build instructions using CMake (#31)
alilleybrinker Sep 11, 2020
f247a23
Fix Doxygen Output (#30)
alilleybrinker Sep 11, 2020
2375013
Bump pact_matching dependency version (#32)
alilleybrinker Oct 5, 2020
6c709fe
Fix CMake build failure by renaming 'FFI' -> 'Ffi' (#33)
cstepanian Oct 6, 2020
41347af
Add FFI wrappers for constructing and deleting MessagePact (#43)
cstepanian Oct 14, 2020
9e30378
Change cbindgen config (#45)
alilleybrinker Oct 22, 2020
9b4f05b
Add FFI methods to get consumer and provider from MessagePact (#47)
cstepanian Oct 27, 2020
4c3f75d
Message pact metadata (#46)
alilleybrinker Oct 29, 2020
4c937f2
Added metadata iteration for message pacts (#48)
alilleybrinker Oct 31, 2020
b2f2799
Updated pact_matching version in ffi
alilleybrinker Nov 4, 2020
5e15927
Add my name to the crate author list (#49)
cstepanian Nov 5, 2020
09bafa7
First pass at basic matching with the FFI. (#50)
alilleybrinker Dec 4, 2020
d53b613
Fixed Cargo.lock
alilleybrinker Dec 4, 2020
d110ecd
Update cbindgen config (#51)
alilleybrinker Dec 9, 2020
851d623
Resolve some warnings during CMake build. (#52)
alilleybrinker Dec 22, 2020
06b0e79
Use absolute paths in macros for hygiene (#53)
cstepanian Dec 25, 2020
104782e
Added logic to detect when not using Cargo nightly (#56)
alilleybrinker Feb 2, 2021
d2e2626
Updated Cargo.lock
alilleybrinker Feb 2, 2021
6fc16c5
We don't want these to run in a fork
alilleybrinker Feb 2, 2021
fd55c67
Added message_new_from_body (#61)
alilleybrinker Feb 23, 2021
6f8e186
Added provider_state_delete function (#60)
alilleybrinker Feb 23, 2021
a07e76a
Added message iterator for message pacts (#76)
alilleybrinker Mar 24, 2021
bd02e36
Upstream prep (#79)
alilleybrinker Apr 22, 2021
44c7b3c
Updated to compile with latest changes
alilleybrinker Apr 22, 2021
95795ad
Restore previously-removed CI files
alilleybrinker Apr 22, 2021
75b2928
Resolved Clippy lints
alilleybrinker Apr 22, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
358 changes: 208 additions & 150 deletions rust/Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
members = [
"pact_models",
"pact_matching",
"pact_matching_ffi",
"pact_mock_server",
"pact_mock_server_cli",
"pact_mock_server_ffi",
Expand Down
1 change: 1 addition & 0 deletions rust/pact_matching_ffi/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build*
156 changes: 156 additions & 0 deletions rust/pact_matching_ffi/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@

# Architecture

The `pact_matching_ffi` crate is built with one goal and a couple constraints.
The goal is that it expose all of `pact_matching` for use by other languages.

The constraints are:

1. It should be non-invasive (meaning it doesn't require changes to
`pact_matching` to, for example, use FFI-friendly types which could be
exposed directly to C).
2. It should introduce limited performance overhead.
3. It should be easy to call from C.
4. It should be easy for other languages to wrap.
5. It should provide strong support for error handling and logging.
6. It should preserve and document any safety guarantees or requirements.

The design of the crate is done with these goals in mind. Before getting into
how the wrapping of `pact_matching` is performed, it's worthwhile to describe
the mechanisms around it.

## Error Handling

Error handling in Rust is robust, using the `Option` and `Result` types to
indicate when data may be missing or errors may occur. Error handling in C
is less powerful, usually using some sentinel values in the return value of
a function to indicate an error has occured. There is also the `errno` API
from the C standard library, which permits reporting of a standardized set
of error codes, but that mechanism doesn't permit custom error information,
and would require us to create a mapping from all possible `pact_matching`
errors into `errno` error codes.

Instead, the error handling system in `pact_matching_ffi` is based around
1) signaling of errors through sentinel values, and 2) permitting C-side
collection of error messages through the `get_error_message` function.
This design is based on the work of Michael F. Bryan in the
[unofficial Rust FFI Guide][ffi_guide], which was based on the error handling
in `libgit2`.

In short: errors are collected from all FFI code, and stored in a
thread-local variable called `LAST_ERROR`. The `get_error_message` function
pulls the latest error message from `LAST_ERROR` and reports it to the
user. All the mechanisms in `pact_matching_ffi/src/error` exist to handle
this error collection and reporting.

## Logging

Logging is a crucial part of any application, and just because `pact_matching`
is being called from another language is no reason to render logging info
from it unavailable. For this reason, `pact_matching_ffi` provides a way for
C callers to initialize a logger and direct logging output to a location of
their choice.

First, they call `logger_init`, which begins the logging setup by creating
a log dispatcher with no sinks. Then they call `logger_attach_sink` to add
sinks, to which any logs matching the filtering level will be sent. Finally,
after all `logger_attach_sink` calls are done, they call `logger_apply` to
apply the logger and complete setup.

The remainder of the code in `pact_matching_ffi/src/log` is plumbing for this
logging setup process.

## The `ffi_fn` macro

FFI functions have to be written with some boilerplate. First, they must be
marked `#[no_mangle]` so their names are exported as-written for C to call.
Second, they must be marked `pub extern` so they're exported and picked up
by `cbindgen`, the tool we use for generating the C header which users of
`pact_matching_ffi` will need.

Then, within the functions themselves, we want to be sure that certain FFI
information is logged, and that errors are captured and reported consistently
and correctly.

For this purpose, we use the `ffi_fn` macro, found in
`pact_matching_ffi/src/util/ffi.rs`. This macro is inspired by a
[macro of the same name][hyper_macro] from the `hyper` crate's C API
(created for use of Hyper in `curl`). Our macro does considerably more work
to capture errors and ensure complete logging, but the idea is the same. The
macro also ensures panics are captured and packaged up like regular errors.
This includes panics due to allocation failure, although we don't do anything
to handle this case specifically.

One thing to note is that most functions using the macro will be written
slightly differently from normal Rust functions. If the function being
wrapped is fallible, then a second block is needed after the function body
block showing what to return in the case of any unexpected failure (usually
either a null pointer of the appropriate `const`-ness and type, or a
sentinel integer value signalling an error occurred).

Here's an example of the macro in use.

```rust
ffi_fn! {
/// Get a mutable pointer to a newly-created default message on the heap.
fn message_new() -> *mut Message {
let message = Message::default();
ptr::raw_to(message)
} {
ptr::null_mut_to::<Message>()
}
}
```

For destructors, or other functions which have no return value, no second
block is required.

```rust
ffi_fn! {
/// Destroy the `Message` being pointed to.
fn message_delete(message: *mut Message) {
ptr::drop_raw(message);
}
}
```

## Opaque Pointers

The `pact_matching_ffi` crate is [_non-invasive_][non_invasive], meaning it
doesn't involve the modification of types in `pact_matching` to be exposable
over the FFI boundary (which would at minimum mean marking them `#[repr(C)]`).
Instead, we expose types in almost all cases as an "opaque pointer," meaning
C knows the _name_ of the type, but not its _contents_, and works only with
pointers to that type.

On the one hand, this is convenient, because it keeps the FFI boundary clear
and separate. On the other hand, it means that C code has to make function
calls to the Rust side rather than accessing fields directly, and that certain
optimizations may be missed because of the indirection. This can be addressed
at least in part through use of cross-language [Link Time Optimization (LTO)][lto]

## Strings

For the most part, to avoid issues with buffer sizing and fallible operations
against output parameters, the `pact_matching_ffi` crate is designed, when
returning strings, to return them as `const char *`, pointing to a
heap-allocated string buffer which must be deleted by calling the provided
`string_delete` function when finished.

One exception is returning the error message, where the user provides a buffer
and the size of that buffer, and the operation to fill the buffer with the
error message may fail. This is because error handling is expected to be very
common, so buffer reuse is ideal.

Additionally, `pact_matching_ffi` returns exclusively UTF-8-encoded strings,
and expects all strings it receives to be UTF-8 encoded.

## Buffers

Whenever `pact_matching_ffi` writes to a buffer, it zeroes out any excess
capacity in the buffer for security reasons.

[ffi_guide]: https://michael-f-bryan.github.io/rust-ffi-guide/errors/index.html
[hyper_macro]: https://github.com/hyperium/hyper/blob/master/src/ffi/macros.rs
[non_invasive]: https://www.possiblerust.com/guide/inbound-outbound-ffi#non-invasive-outbound-ffi
[lto]: http://blog.llvm.org/2019/09/closing-gap-cross-language-lto-between.html
Loading