Skip to content

Commit

Permalink
Example of OpenTelemetry FFI Bridge for Rust and C++ and Performance …
Browse files Browse the repository at this point in the history
…Benchmark (#313)
  • Loading branch information
gscalderonl authored Aug 5, 2023
1 parent e796881 commit 37e4466
Show file tree
Hide file tree
Showing 10 changed files with 256 additions and 0 deletions.
10 changes: 10 additions & 0 deletions examples/ffi/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "ffi"
version = "0.1.0"
edition = "2021"

[dependencies]
cxx = "1.0"

[build-dependencies]
cxx-build = "1.0"
37 changes: 37 additions & 0 deletions examples/ffi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# OpenTelemetry FFI Bridge for Rust and C++

This code demonstrates the creation of a Foreign Function Interface (FFI) bridge between Rust and C++ using the [cxx crate](https://github.com/dtolnay/cxx). The primary goal is to facilitate interoperability between Rust and C++ codebases, particularly in the context of OpenTelemetry.

## Code Overview

### Create an FFI Bridge

The cxx crate is utilized to establish an FFI bridge between Rust and C++. A Rust module named ffi is defined using the #[cxx::bridge] attribute. Within this module, a Rust struct named TracerProvider is defined. This struct encapsulates a name field and serves as a data structure that can be accessed from C++.

An FFI function named get_tracer_provider() is declared within the extern "Rust" block. This function is designed to be callable from C++ and is expected to return a reference to a TracerProvider instance.

### Rust TracerProvider Implementation

A Rust struct named RustTracerProvider is introduced. This struct is designed to manage a TracerProvider instance. It includes a new() method that initializes a new RustTracerProvider with a default TracerProvider.

### Bridging Rust and C++ with a C Wrapper for OpenTelemetry Integration

By now CXX has some types that are intended to be supported "soon" but are just not implemented yet.

These types are necessary for the instrumentation and configuration of the C++ and Rust interaction within the context of OpenTelemetry.

To mitigate these issues currently, we can follow an approach by creating a C wrapper that acts as an intermediary between Rust and C++. This wrapper will handle these specific types, ensuring a smooth interaction between the two languages.

### Creating a C Wrapper for Rust and C++ Interaction

To address the impending types and provide a way to bridge Rust and C++ effectively, we propose the following steps:

1. **Define the C Interface**: Begin by defining a C interface that will be accessible from both Rust and C++ codebases. This interface should include functions and structures that mirror the expected types. These functions will serve as an abstraction layer for handling the complex types.

2. **Implement the C Wrapper**: In a separate C source file, implement the functions defined in the C interface. The implementation will act as an intermediary between Rust and C++, converting the data structures as needed and making the interaction seamless.

3. **Expose the C Wrapper to Rust and C++**: To access the C wrapper, expose its functions through the FFI mechanisms provided by Rust and C++. This involves creating external function declarations in Rust and including the C header in the C++ codebase.

4. **Utilize the C Wrapper**: In both the Rust and C++ code, replace the direct usage of the "soon-to-be-supported" types with calls to the C wrapper's functions. This ensures that data is properly converted and handled, regardless of the current limitations of CXX.

By adopting this approach, we can ensure a reliable and consistent interaction between Rust and C++ while awaiting the full support of the intended types in CXX. This strategy not only mitigates the current issues but also paves the way for a smoother transition once the desired types are officially implemented.
2 changes: 2 additions & 0 deletions examples/ffi/cpprust/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/target
/Cargo.lock
15 changes: 15 additions & 0 deletions examples/ffi/cpprust/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "cpprust"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["staticlib"]

[dependencies]
cxx = "1.0"
log = "0.4"
env_logger = "0.9"

[build-dependencies]
cxx-build = "1.0"
29 changes: 29 additions & 0 deletions examples/ffi/cpprust/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Rust and C++ Logging Interop Benchmark

This section contains a benchmarking program that evaluates the performance of using the Foreign Function Interface (FFI) to pass logging data between Rust and C++. The primary focus is to compare the FFI cost when logging data from C++ to Rust against using a logging library directly in C++.

## Benchmark Results

The benchmark results showcase the execution time and CPU cycles for various logging scenarios. Each benchmark assesses the impact of using the Foreign Function Interface (FFI) to pass data between Rust and C++ for logging operations.

| Benchmark | Time (ns) | CPU (ns) | Iterations |
|------------------------------------------|-----------|----------|----------------|
| BM_log_string_from_cpp_to_rust_log_crate | 0.821 | 0.758 | 924,897,039 |
| BM_log_int_from_cpp_to_rust_log_crate | 0.859 | 0.793 | 900,003,214 |
| BM_log_vector_from_cpp_to_rust_log_crate | 0.822 | 0.759 | 910,954,225 |
| BM_log_struct_from_cpp_to_rust_log_crate | 0.748 | 0.691 | 1,000,000,000 |
| BM_log_class_from_cpp_to_rust_log_crate | 1.92 | 1.78 | 388,112,879 |

## Analysis

### FFI Cost Comparison

The benchmark results reveal that utilizing the Foreign Function Interface (FFI) to interface between Rust and C++ for logging introduces a moderate increase in execution time and CPU cycles, approximately 70-80% higher than directly using a logging library in C++.

### Real-world Context

In practical logging scenarios where logs are typically transmitted to files or over networks, the FFI overhead remains inconsequential. For instance, considering a situation where 1,000,000 log records are sent per second, the added FFI interop layer contributes a mere 3 to 12 CPU cycles per log API invocation. Cumulatively, this additional computational impact translates to approximately 1% of the total CPU cycles, which is highly affordable and well within acceptable limits for efficient logging operations.

## Conclusion

The findings from the benchmarking exercise underscore the efficiency and practicality of employing FFI for logging purposes. While there exists a measured increase in execution time and CPU cycles, the overall impact remains negligible and aligns well with real-world logging scenarios. The interoperation between Rust and C++ using FFI proves to be a viable and efficient solution, providing a seamless bridge for logging tasks with minimal performance overhead.
9 changes: 9 additions & 0 deletions examples/ffi/cpprust/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
fn main()
{
cxx_build::bridge("src/lib.rs")
.file("src/animal.hpp")
.compile("cpp_from_rust");

println!("cargo:rerun-if-changed=src/lib.rs");
println!("cargo:rerun-if-changed=src/animal.hpp");
}
8 changes: 8 additions & 0 deletions examples/ffi/cpprust/src/animal.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class Animal {
public:
Animal(int age) : value(age) {}
int get_age() const {return value;}

private:
int value;
};
56 changes: 56 additions & 0 deletions examples/ffi/cpprust/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#[macro_use]
extern crate log;
extern crate env_logger;
use cxx::{CxxString, CxxVector};

#[cxx::bridge]
mod ffi {
struct Animal {
value: i32,
}

struct Person {
name: String,
age: i32,
}

extern "Rust" {
fn log_string_from_cpp_to_rust_log_crate(message: &CxxString);
fn log_int_from_cpp_to_rust_log_crate(level: i32);
fn log_vector_from_cpp_to_rust_log_crate(attributes: &CxxVector<CxxString>);
fn log_struct_from_cpp_to_rust_log_crate(person: &Person);
fn log_class_from_cpp_to_rust_log_crate(animal: &Animal);
fn init_rust_logger() -> ();
}

unsafe extern "C++" {
include!("cpprust/src/animal.hpp");
type Animal;
fn get_age(&self) -> i32;
}
}

pub fn log_string_from_cpp_to_rust_log_crate(message: &CxxString) {
info!("{}", message);
}

pub fn log_int_from_cpp_to_rust_log_crate(level: i32) {
info!("{}", level);
}

pub fn log_vector_from_cpp_to_rust_log_crate(attributes: &CxxVector<CxxString>) {
info!("{:?}", attributes);
}

pub fn log_struct_from_cpp_to_rust_log_crate(person: &ffi::Person) {
info!("Received persons: {} who is {} years old", person.name, person.age);
}

pub fn log_class_from_cpp_to_rust_log_crate(animal: &ffi::Animal) {
let value = animal.get_age();
info!("{}", value);
}

pub fn init_rust_logger() -> () {
env_logger::init();
}
60 changes: 60 additions & 0 deletions examples/ffi/cpprust/src/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#include "cpprust/src/lib.rs.h"
#include <benchmark/benchmark.h>
#include <vector>
#include <string>
#include <iostream>

static void BM_log_string_from_cpp_to_rust_log_crate(benchmark::State& state) {
std::string message = "Test";
for (auto _ : state) {
log_string_from_cpp_to_rust_log_crate(message);
}
}

static void BM_log_int_from_cpp_to_rust_log_crate(benchmark::State& state) {
int level = 1;

for (auto _ : state) {
log_int_from_cpp_to_rust_log_crate(level);
}
}

static void BM_log_vector_from_cpp_to_rust_log_crate(benchmark::State& state) {
std::vector<std::string> names = {"Alice", "Bob", "Charlie"};

for (auto _ : state) {
log_vector_from_cpp_to_rust_log_crate(names);
}
}

static void BM_log_struct_from_cpp_to_rust_log_crate(benchmark::State& state) {
Person p1;
p1.name = "John";
p1.age = 30;

for(auto _ : state) {
log_struct_from_cpp_to_rust_log_crate(p1);
}
}

static void BM_log_class_from_cpp_to_rust_log_crate(benchmark::State& state) {
Animal dog(42);

for(auto _ : state) {
log_class_from_cpp_to_rust_log_crate(dog);
}
}

BENCHMARK(BM_log_string_from_cpp_to_rust_log_crate);
BENCHMARK(BM_log_int_from_cpp_to_rust_log_crate);
BENCHMARK(BM_log_vector_from_cpp_to_rust_log_crate);
BENCHMARK(BM_log_struct_from_cpp_to_rust_log_crate);
BENCHMARK(BM_log_class_from_cpp_to_rust_log_crate);

int main(int argc, char** argv) {
init_rust_logger();
::benchmark::Initialize(&argc, argv);
::benchmark::RunSpecifiedBenchmarks();

return 0;
}
30 changes: 30 additions & 0 deletions examples/ffi/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use opentelemetry::sdk::trace::TracerProvider;

#[cxx::bridge]
mod ffi {
struct TracerProvider {
name: String,
}

extern "Rust" {
fn get_tracer_provider() -> &TracerProvider;
}
}

#[derive(Default)]
pub struct RustTracerProvider {
provider: TracerProvider,
}

impl RustTracerProvider {
pub fn new() -> Self {
Self {
provider: TracerProvider::default(),
}
}
}

pub fn get_tracer_provider() -> *mut TracerProvider {
let provider = Box::new(RustTracerProvider::new().provider);
Box::into_raw(provider)
}

0 comments on commit 37e4466

Please sign in to comment.