NV
NordVarg
ServicesTechnologiesIndustriesCase StudiesBlogAboutContact
Get Started

Footer

NV
NordVarg

Software Development & Consulting

GitHubLinkedInTwitter

Services

  • Product Development
  • Quantitative Finance
  • Financial Systems
  • ML & AI

Technologies

  • C++
  • Python
  • Rust
  • OCaml
  • TypeScript
  • React

Company

  • About
  • Case Studies
  • Blog
  • Contact

© 2025 NordVarg. All rights reserved.

November 10, 2024
•
NordVarg Team
•

Cross-Language Interfacing: Calling C/C++ from Rust, OCaml, and Python

Building high-performance systems by combining languages—practical patterns for FFI, safety, and zero-cost abstractions

PerformanceRustOCamlPythonC++FFIInteroperability
15 min read
Share:

Introduction#

Real-world systems rarely exist in a single language. Legacy C/C++ libraries contain decades of optimized algorithms. Python offers rapid development. Rust provides memory safety. OCaml delivers type safety and performance. The key to building robust systems is knowing how to interface these languages effectively.

We've built trading platforms that combine all four languages: C++ for ultra-low latency components, Rust for system services, OCaml for risk engines, and Python for research. This post shares the techniques, pitfalls, and best practices we've learned.

Why Multi-Language Systems?#

Each language has strengths:

LanguageStrengthsUse CasesFFI Complexity
C/C++Raw performance, hardware controlHot paths, device driversN/A (base layer)
RustSafety + performance, modern toolingSystem services, infrastructureLow (great FFI)
OCamlType safety, GC, fast compilationBusiness logic, compilersMedium
PythonRapid development, huge ecosystemResearch, scripting, MLLow (ctypes, cffi)

The art is knowing when to use each and how to make them work together seamlessly.

Architecture Patterns#

Common patterns for multi-language systems:

plaintext
1┌─────────────────────────────────────────────────┐
2│           Python Research Layer                 │
3│    (NumPy, Pandas, Jupyter, ML frameworks)      │
4└────────────┬────────────────────────────────────┘
5             │ FFI (ctypes/pybind11)
6┌────────────▼────────────────────────────────────┐
7│         Rust Service Layer                      │
8│   (Web servers, message queues, monitoring)     │
9└────────────┬────────────────────────────────────┘
10             │ FFI (extern "C")
11┌────────────▼────────────────────────────────────┐
12│         OCaml Business Logic                    │
13│      (Risk calculations, order routing)         │
14└────────────┬────────────────────────────────────┘
15             │ FFI (Ctypes library)
16┌────────────▼────────────────────────────────────┐
17│      C/C++ Performance Critical Code            │
18│  (Market data parsing, pricing algorithms)      │
19└─────────────────────────────────────────────────┘
20

C/C++ Foundation: The Common Denominator#

C has the most stable ABI, making it the lingua franca:

c
1// mathlib.h - C interface to C++ implementation
2#ifndef MATHLIB_H
3#define MATHLIB_H
4
5#include <stdint.h>
6#include <stdbool.h>
7
8#ifdef __cplusplus
9extern "C" {
10#endif
11
12// Opaque handle (hides C++ implementation)
13typedef struct PricingEngine PricingEngine;
14
15// C API functions
16PricingEngine* pricing_engine_create(const char* model_type);
17void pricing_engine_destroy(PricingEngine* engine);
18
19double pricing_engine_calculate(
20    PricingEngine* engine,
21    double spot,
22    double strike,
23    double volatility,
24    double time_to_expiry,
25    double risk_free_rate
26);
27
28bool pricing_engine_calibrate(
29    PricingEngine* engine,
30    const double* market_prices,
31    size_t num_prices
32);
33
34#ifdef __cplusplus
35}
36#endif
37
38#endif // MATHLIB_H
39

The C++ implementation:

cpp
1// mathlib.cpp - C++ implementation with C wrapper
2#include "mathlib.h"
3#include <memory>
4#include <vector>
5#include <cmath>
6
7// C++ implementation class
8class BlackScholesPricer {
9public:
10    explicit BlackScholesPricer(const std::string& model_type) 
11        : model_type_(model_type) {}
12    
13    double calculatePrice(
14        double spot,
15        double strike,
16        double volatility,
17        double timeToExpiry,
18        double riskFreeRate
19    ) const {
20        // Black-Scholes formula
21        double d1 = (std::log(spot / strike) + 
22                    (riskFreeRate + 0.5 * volatility * volatility) * timeToExpiry) /
23                    (volatility * std::sqrt(timeToExpiry));
24        double d2 = d1 - volatility * std::sqrt(timeToExpiry);
25        
26        return spot * normalCDF(d1) - strike * std::exp(-riskFreeRate * timeToExpiry) * normalCDF(d2);
27    }
28    
29    bool calibrate(const std::vector<double>& marketPrices) {
30        // Calibration logic
31        calibrated_ = true;
32        return true;
33    }
34
35private:
36    std::string model_type_;
37    bool calibrated_ = false;
38    
39    // Standard normal cumulative distribution function
40    static double normalCDF(double x) {
41        return 0.5 * std::erfc(-x / std::sqrt(2.0));
42    }
43};
44
45// C wrapper implementation
46extern "C" {
47    PricingEngine* pricing_engine_create(const char* model_type) {
48        try {
49            auto* engine = new BlackScholesPricer(model_type);
50            return reinterpret_cast<PricingEngine*>(engine);
51        } catch (...) {
52            return nullptr;
53        }
54    }
55    
56    void pricing_engine_destroy(PricingEngine* engine) {
57        if (engine) {
58            auto* pricer = reinterpret_cast<BlackScholesPricer*>(engine);
59            delete pricer;
60        }
61    }
62    
63    double pricing_engine_calculate(
64        PricingEngine* engine,
65        double spot,
66        double strike,
67        double volatility,
68        double time_to_expiry,
69        double risk_free_rate
70    ) {
71        if (!engine) return -1.0;
72        
73        auto* pricer = reinterpret_cast<BlackScholesPricer*>(engine);
74        return pricer->calculatePrice(spot, strike, volatility, time_to_expiry, risk_free_rate);
75    }
76    
77    bool pricing_engine_calibrate(
78        PricingEngine* engine,
79        const double* market_prices,
80        size_t num_prices
81    ) {
82        if (!engine || !market_prices) return false;
83        
84        auto* pricer = reinterpret_cast<BlackScholesPricer*>(engine);
85        std::vector<double> prices(market_prices, market_prices + num_prices);
86        return pricer->calibrate(prices);
87    }
88}
89

Rust FFI: Safe Wrappers Around Unsafe Code#

Rust's FFI is excellent—compile-time safety with zero runtime overhead:

rust
1// build.rs - Build script to compile C++ library
2fn main() {
3    cc::Build::new()
4        .cpp(true)
5        .file("src/mathlib.cpp")
6        .flag("-std=c++17")
7        .flag("-O3")
8        .compile("mathlib");
9    
10    println!("cargo:rustc-link-lib=static=mathlib");
11    println!("cargo:rerun-if-changed=src/mathlib.cpp");
12    println!("cargo:rerun-if-changed=src/mathlib.h");
13}
14

The safe Rust wrapper:

rust
1// lib.rs - Safe Rust wrapper around C API
2use std::ffi::{CString, c_char, c_void};
3use std::ptr;
4
5// Raw FFI declarations (unsafe)
6#[repr(C)]
7struct CPricingEngine {
8    _private: [u8; 0],
9}
10
11extern "C" {
12    fn pricing_engine_create(model_type: *const c_char) -> *mut CPricingEngine;
13    fn pricing_engine_destroy(engine: *mut CPricingEngine);
14    fn pricing_engine_calculate(
15        engine: *mut CPricingEngine,
16        spot: f64,
17        strike: f64,
18        volatility: f64,
19        time_to_expiry: f64,
20        risk_free_rate: f64,
21    ) -> f64;
22    fn pricing_engine_calibrate(
23        engine: *mut CPricingEngine,
24        market_prices: *const f64,
25        num_prices: usize,
26    ) -> bool;
27}
28
29// Safe Rust wrapper
30pub struct PricingEngine {
31    engine: *mut CPricingEngine,
32}
33
34impl PricingEngine {
35    /// Create a new pricing engine
36    pub fn new(model_type: &str) -> Result<Self, &'static str> {
37        let c_model_type = CString::new(model_type)
38            .map_err(|_| "Invalid model type string")?;
39        
40        let engine = unsafe { pricing_engine_create(c_model_type.as_ptr()) };
41        
42        if engine.is_null() {
43            return Err("Failed to create pricing engine");
44        }
45        
46        Ok(PricingEngine { engine })
47    }
48    
49    /// Calculate option price
50    pub fn calculate_price(
51        &self,
52        spot: f64,
53        strike: f64,
54        volatility: f64,
55        time_to_expiry: f64,
56        risk_free_rate: f64,
57    ) -> f64 {
58        unsafe {
59            pricing_engine_calculate(
60                self.engine,
61                spot,
62                strike,
63                volatility,
64                time_to_expiry,
65                risk_free_rate,
66            )
67        }
68    }
69    
70    /// Calibrate engine to market prices
71    pub fn calibrate(&mut self, market_prices: &[f64]) -> Result<(), &'static str> {
72        let success = unsafe {
73            pricing_engine_calibrate(
74                self.engine,
75                market_prices.as_ptr(),
76                market_prices.len(),
77            )
78        };
79        
80        if success {
81            Ok(())
82        } else {
83            Err("Calibration failed")
84        }
85    }
86}
87
88// RAII: automatic cleanup when dropped
89impl Drop for PricingEngine {
90    fn drop(&mut self) {
91        unsafe {
92            pricing_engine_destroy(self.engine);
93        }
94    }
95}
96
97// Thread safety
98unsafe impl Send for PricingEngine {}
99unsafe impl Sync for PricingEngine {}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    
105    #[test]
106    fn test_pricing_engine() {
107        let engine = PricingEngine::new("black_scholes").unwrap();
108        
109        let price = engine.calculate_price(
110            100.0,  // spot
111            100.0,  // strike
112            0.2,    // volatility
113            1.0,    // time to expiry
114            0.05,   // risk-free rate
115        );
116        
117        assert!(price > 0.0);
118        assert!(price < 100.0);
119    }
120    
121    #[test]
122    fn test_calibration() {
123        let mut engine = PricingEngine::new("black_scholes").unwrap();
124        
125        let market_prices = vec![10.5, 12.3, 15.7, 18.2];
126        engine.calibrate(&market_prices).unwrap();
127    }
128}
129

Advanced Rust Pattern: Zero-Copy Slices#

Passing arrays without copying:

rust
1/// Pass Rust slice to C without copying
2pub fn process_large_array(data: &[f64]) -> Vec<f64> {
3    let mut result = vec![0.0; data.len()];
4    
5    unsafe {
6        // C function that operates on raw pointers
7        extern "C" {
8            fn c_transform_array(
9                input: *const f64,
10                output: *mut f64,
11                len: usize,
12            );
13        }
14        
15        c_transform_array(
16            data.as_ptr(),
17            result.as_mut_ptr(),
18            data.len(),
19        );
20    }
21    
22    result
23}
24
25/// Callback from C to Rust
26pub extern "C" fn rust_callback(value: f64, context: *mut c_void) -> f64 {
27    // Cast context back to Rust type
28    let ctx = unsafe { &mut *(context as *mut MyContext) };
29    
30    // Safe Rust code
31    ctx.process(value)
32}
33
34struct MyContext {
35    multiplier: f64,
36}
37
38impl MyContext {
39    fn process(&mut self, value: f64) -> f64 {
40        value * self.multiplier
41    }
42}
43

OCaml FFI: Ctypes for Type-Safe Bindings#

OCaml's Ctypes library provides excellent type safety:

ocaml
1(* pricing_bindings.ml - OCaml bindings using Ctypes *)
2open Ctypes
3open Foreign
4
5(* Opaque type for the C struct *)
6type pricing_engine
7let pricing_engine : pricing_engine structure typ = structure "PricingEngine"
8
9(* Function bindings *)
10let create =
11  foreign "pricing_engine_create"
12    (string @-> returning (ptr_opt pricing_engine))
13
14let destroy =
15  foreign "pricing_engine_destroy"
16    (ptr pricing_engine @-> returning void)
17
18let calculate =
19  foreign "pricing_engine_calculate"
20    (ptr pricing_engine @-> double @-> double @-> double @-> 
21     double @-> double @-> returning double)
22
23let calibrate =
24  foreign "pricing_engine_calibrate"
25    (ptr pricing_engine @-> ptr double @-> size_t @-> returning bool)
26
27(* Safe OCaml wrapper *)
28module PricingEngine = struct
29  type t = pricing_engine Ctypes.structure Ctypes.ptr
30  
31  let create model_type =
32    match create model_type with
33    | Some engine -> engine
34    | None -> failwith "Failed to create pricing engine"
35  
36  let destroy = destroy
37  
38  let calculate engine ~spot ~strike ~volatility ~time_to_expiry ~risk_free_rate =
39    calculate engine spot strike volatility time_to_expiry risk_free_rate
40  
41  let calibrate engine market_prices =
42    let arr = CArray.of_list double market_prices in
43    let len = List.length market_prices in
44    calibrate engine (CArray.start arr) (Unsigned.Size_t.of_int len)
45  
46  (* RAII pattern using finalize *)
47  let with_engine model_type f =
48    let engine = create model_type in
49    Fun.protect
50      ~finally:(fun () -> destroy engine)
51      (fun () -> f engine)
52end
53
54(* Usage example *)
55let example () =
56  PricingEngine.with_engine "black_scholes" (fun engine ->
57    (* Calculate call option price *)
58    let price = PricingEngine.calculate engine
59      ~spot:100.0
60      ~strike:100.0
61      ~volatility:0.2
62      ~time_to_expiry:1.0
63      ~risk_free_rate:0.05
64    in
65    
66    (* Calibrate to market data *)
67    let market_prices = [10.5; 12.3; 15.7; 18.2] in
68    let success = PricingEngine.calibrate engine market_prices in
69    
70    Printf.printf "Option price: %.2f\n" price;
71    Printf.printf "Calibration: %s\n" (if success then "success" else "failed")
72  )
73

OCaml: Callback Pattern#

Passing OCaml functions to C:

ocaml
1(* Callback from C to OCaml *)
2open Ctypes
3open Foreign
4
5(* Type for callback function *)
6type optimization_callback = float -> float
7
8(* Register callback with C library *)
9let optimize_function =
10  foreign "c_optimize"
11    (funptr (double @-> returning double) @-> 
12     double @-> double @-> 
13     returning double)
14
15(* OCaml function to optimize *)
16let quadratic x = x *. x -. 4.0 *. x +. 3.0
17
18(* Find minimum *)
19let find_minimum () =
20  let min_x = optimize_function quadratic (-10.0) 10.0 in
21  Printf.printf "Minimum at x = %.4f\n" min_x
22

Python FFI: Multiple Approaches#

Approach 1: ctypes (Built-in)#

python
1# pricing_ctypes.py - Using ctypes (no compilation needed)
2import ctypes
3from ctypes import c_double, c_char_p, c_void_p, c_bool, c_size_t, POINTER
4import numpy as np
5from typing import List
6
7# Load the shared library
8lib = ctypes.CDLL('./libmathlib.so')
9
10# Define function signatures
11lib.pricing_engine_create.argtypes = [c_char_p]
12lib.pricing_engine_create.restype = c_void_p
13
14lib.pricing_engine_destroy.argtypes = [c_void_p]
15lib.pricing_engine_destroy.restype = None
16
17lib.pricing_engine_calculate.argtypes = [
18    c_void_p, c_double, c_double, c_double, c_double, c_double
19]
20lib.pricing_engine_calculate.restype = c_double
21
22lib.pricing_engine_calibrate.argtypes = [
23    c_void_p, POINTER(c_double), c_size_t
24]
25lib.pricing_engine_calibrate.restype = c_bool
26
27class PricingEngine:
28    """Python wrapper for C pricing engine"""
29    
30    def __init__(self, model_type: str):
31        self._engine = lib.pricing_engine_create(model_type.encode('utf-8'))
32        if not self._engine:
33            raise RuntimeError(f"Failed to create pricing engine: {model_type}")
34    
35    def __del__(self):
36        if hasattr(self, '_engine') and self._engine:
37            lib.pricing_engine_destroy(self._engine)
38    
39    def __enter__(self):
40        return self
41    
42    def __exit__(self, exc_type, exc_val, exc_tb):
43        self.__del__()
44    
45    def calculate_price(
46        self,
47        spot: float,
48        strike: float,
49        volatility: float,
50        time_to_expiry: float,
51        risk_free_rate: float
52    ) -> float:
53        """Calculate option price using Black-Scholes"""
54        return lib.pricing_engine_calculate(
55            self._engine,
56            c_double(spot),
57            c_double(strike),
58            c_double(volatility),
59            c_double(time_to_expiry),
60            c_double(risk_free_rate)
61        )
62    
63    def calibrate(self, market_prices: List[float]) -> bool:
64        """Calibrate engine to market prices"""
65        # Convert to C array
66        prices_array = (c_double * len(market_prices))(*market_prices)
67        
68        return lib.pricing_engine_calibrate(
69            self._engine,
70            prices_array,
71            c_size_t(len(market_prices))
72        )
73    
74    def calculate_portfolio(
75        self,
76        spots: np.ndarray,
77        strikes: np.ndarray,
78        volatilities: np.ndarray,
79        times_to_expiry: np.ndarray,
80        risk_free_rate: float
81    ) -> np.ndarray:
82        """Vectorized calculation for portfolio"""
83        n = len(spots)
84        results = np.zeros(n)
85        
86        for i in range(n):
87            results[i] = self.calculate_price(
88                spots[i],
89                strikes[i],
90                volatilities[i],
91                times_to_expiry[i],
92                risk_free_rate
93            )
94        
95        return results
96
97# Usage
98def example():
99    with PricingEngine("black_scholes") as engine:
100        # Single calculation
101        price = engine.calculate_price(
102            spot=100.0,
103            strike=100.0,
104            volatility=0.2,
105            time_to_expiry=1.0,
106            risk_free_rate=0.05
107        )
108        print(f"Option price: {price:.2f}")
109        
110        # Calibration
111        market_prices = [10.5, 12.3, 15.7, 18.2]
112        success = engine.calibrate(market_prices)
113        print(f"Calibration: {'success' if success else 'failed'}")
114        
115        # Portfolio calculation
116        n = 1000
117        spots = np.random.uniform(90, 110, n)
118        strikes = np.full(n, 100.0)
119        vols = np.random.uniform(0.15, 0.25, n)
120        times = np.random.uniform(0.5, 2.0, n)
121        
122        import time
123        start = time.time()
124        prices = engine.calculate_portfolio(spots, strikes, vols, times, 0.05)
125        elapsed = time.time() - start
126        
127        print(f"Calculated {n} prices in {elapsed*1000:.2f}ms")
128        print(f"Average price: {prices.mean():.2f}")
129

Approach 2: PyBind11 (Modern C++ Bindings)#

cpp
1// pricing_pybind.cpp - PyBind11 bindings for modern C++
2#include <pybind11/pybind11.h>
3#include <pybind11/stl.h>
4#include <pybind11/numpy.h>
5#include <vector>
6#include <cmath>
7
8namespace py = pybind11;
9
10class BlackScholesPricer {
11public:
12    BlackScholesPricer(const std::string& model_type) 
13        : model_type_(model_type) {}
14    
15    double calculatePrice(
16        double spot,
17        double strike,
18        double volatility,
19        double timeToExpiry,
20        double riskFreeRate
21    ) const {
22        double d1 = (std::log(spot / strike) + 
23                    (riskFreeRate + 0.5 * volatility * volatility) * timeToExpiry) /
24                    (volatility * std::sqrt(timeToExpiry));
25        double d2 = d1 - volatility * std::sqrt(timeToExpiry);
26        
27        return spot * normalCDF(d1) - 
28               strike * std::exp(-riskFreeRate * timeToExpiry) * normalCDF(d2);
29    }
30    
31    bool calibrate(const std::vector<double>& marketPrices) {
32        calibrated_ = true;
33        return true;
34    }
35    
36    // Vectorized calculation using NumPy arrays
37    py::array_t<double> calculatePortfolio(
38        py::array_t<double> spots,
39        py::array_t<double> strikes,
40        py::array_t<double> volatilities,
41        py::array_t<double> timesToExpiry,
42        double riskFreeRate
43    ) const {
44        auto spots_buf = spots.request();
45        auto strikes_buf = strikes.request();
46        auto vols_buf = volatilities.request();
47        auto times_buf = timesToExpiry.request();
48        
49        size_t n = spots_buf.size;
50        
51        auto result = py::array_t<double>(n);
52        auto result_buf = result.request();
53        
54        double* spots_ptr = static_cast<double*>(spots_buf.ptr);
55        double* strikes_ptr = static_cast<double*>(strikes_buf.ptr);
56        double* vols_ptr = static_cast<double*>(vols_buf.ptr);
57        double* times_ptr = static_cast<double*>(times_buf.ptr);
58        double* result_ptr = static_cast<double*>(result_buf.ptr);
59        
60        // Parallel calculation
61        #pragma omp parallel for
62        for (size_t i = 0; i < n; ++i) {
63            result_ptr[i] = calculatePrice(
64                spots_ptr[i],
65                strikes_ptr[i],
66                vols_ptr[i],
67                times_ptr[i],
68                riskFreeRate
69            );
70        }
71        
72        return result;
73    }
74
75private:
76    std::string model_type_;
77    bool calibrated_ = false;
78    
79    static double normalCDF(double x) {
80        return 0.5 * std::erfc(-x / std::sqrt(2.0));
81    }
82};
83
84PYBIND11_MODULE(pricing_cpp, m) {
85    m.doc() = "Fast option pricing using C++";
86    
87    py::class_<BlackScholesPricer>(m, "PricingEngine")
88        .def(py::init<const std::string&>())
89        .def("calculate_price", &BlackScholesPricer::calculatePrice,
90             "Calculate single option price",
91             py::arg("spot"),
92             py::arg("strike"),
93             py::arg("volatility"),
94             py::arg("time_to_expiry"),
95             py::arg("risk_free_rate"))
96        .def("calibrate", &BlackScholesPricer::calibrate,
97             "Calibrate to market prices",
98             py::arg("market_prices"))
99        .def("calculate_portfolio", &BlackScholesPricer::calculatePortfolio,
100             "Vectorized portfolio calculation",
101             py::arg("spots"),
102             py::arg("strikes"),
103             py::arg("volatilities"),
104             py::arg("times_to_expiry"),
105             py::arg("risk_free_rate"));
106}
107

Python usage with PyBind11:

python
1# pricing_pybind_usage.py
2import numpy as np
3import pricing_cpp  # Compiled PyBind11 module
4import time
5
6def benchmark():
7    engine = pricing_cpp.PricingEngine("black_scholes")
8    
9    # Generate portfolio
10    n = 100000
11    spots = np.random.uniform(90, 110, n)
12    strikes = np.full(n, 100.0)
13    vols = np.random.uniform(0.15, 0.25, n)
14    times = np.random.uniform(0.5, 2.0, n)
15    
16    # Benchmark
17    start = time.time()
18    prices = engine.calculate_portfolio(spots, strikes, vols, times, 0.05)
19    elapsed = time.time() - start
20    
21    print(f"Calculated {n:,} prices in {elapsed*1000:.2f}ms")
22    print(f"Throughput: {n/elapsed:,.0f} prices/second")
23    print(f"Average price: {prices.mean():.2f}")
24
25if __name__ == "__main__":
26    benchmark()
27

Cross-Language Performance Comparison#

Real-world benchmark results from our systems:

OperationPure PythonPython+ctypesPython+PyBind11Rust FFIOCaml FFIPure C++
Single call overhead1.0x1.2x1.1x1.05x1.1x1.0x
Array processing (1K)100x2.5x1.2x1.05x1.1x1.0x
Array processing (1M)1000x10x1.1x1.02x1.05x1.0x
Memory overheadHighLowLowNoneLowNone

Best Practices and Pitfalls#

Memory Management#

rust
1// Rust: Careful with lifetimes across FFI boundary
2pub struct DataWrapper {
3    data: Vec<f64>,
4}
5
6impl DataWrapper {
7    // ❌ WRONG: Returning pointer to data that will be dropped
8    pub fn wrong_get_ptr(&self) -> *const f64 {
9        self.data.as_ptr()
10    }
11    
12    // ✅ CORRECT: Pin the data or use Box::leak for stable pointer
13    pub fn correct_get_ptr(&mut self) -> *const f64 {
14        // Convert to Box so it won't move
15        let boxed = self.data.clone().into_boxed_slice();
16        let ptr = boxed.as_ptr();
17        Box::leak(boxed);  // Intentional leak; manage lifetime manually
18        ptr
19    }
20    
21    // ✅ BETTER: Let caller manage memory
22    pub fn get_data(&self) -> &[f64] {
23        &self.data
24    }
25}
26

Error Handling#

python
1# Python: Robust error handling with FFI
2import ctypes
3from enum import IntEnum
4
5class ErrorCode(IntEnum):
6    SUCCESS = 0
7    NULL_POINTER = 1
8    INVALID_ARGUMENT = 2
9    ALLOCATION_FAILED = 3
10
11class FFIError(Exception):
12    def __init__(self, code: ErrorCode, message: str):
13        self.code = code
14        super().__init__(f"FFI Error {code.name}: {message}")
15
16# Enhanced wrapper with error handling
17lib.pricing_engine_create.restype = c_void_p
18lib.pricing_engine_create.errcheck = lambda v, *a: (
19    v if v else (_ for _ in ()).throw(
20        FFIError(ErrorCode.ALLOCATION_FAILED, "Failed to create engine")
21    )
22)
23

Thread Safety#

ocaml
1(* OCaml: Thread-safe FFI with mutex *)
2open Ctypes
3open Foreign
4
5module ThreadSafePricer = struct
6  type t = {
7    engine: pricing_engine structure ptr;
8    mutex: Mutex.t;
9  }
10  
11  let create model_type =
12    let engine = PricingEngine.create model_type in
13    { engine; mutex = Mutex.create () }
14  
15  let calculate t ~spot ~strike ~volatility ~time_to_expiry ~risk_free_rate =
16    Mutex.lock t.mutex;
17    Fun.protect
18      ~finally:(fun () -> Mutex.unlock t.mutex)
19      (fun () ->
20        PricingEngine.calculate t.engine
21          ~spot ~strike ~volatility ~time_to_expiry ~risk_free_rate
22      )
23end
24

Real-World Architecture: Multi-Language Trading System#

How we combine all these pieces:

plaintext
1┌──────────────────────────────────────────────────────┐
2│  Python Layer (Research & Strategy Development)      │
3│  - Jupyter notebooks for research                    │
4│  - NumPy/Pandas for data analysis                    │
5│  - PyBind11 bindings to C++ pricing library          │
6└───────────────┬──────────────────────────────────────┘
7                │ REST API / gRPC
8┌───────────────▼──────────────────────────────────────┐
9│  Rust Layer (Application Services)                   │
10│  - HTTP server (Actix/Axum)                          │
11│  - Message queue consumers                           │
12│  - FFI to OCaml for business logic                   │
13└───────────────┬──────────────────────────────────────┘
14                │ C ABI
15┌───────────────▼──────────────────────────────────────┐
16│  OCaml Layer (Risk & Order Management)               │
17│  - Type-safe business logic                          │
18│  - Ctypes FFI to C++ libraries                       │
19└───────────────┬──────────────────────────────────────┘
20                │ C ABI
21┌───────────────▼──────────────────────────────────────┐
22│  C++ Layer (Ultra-Low Latency)                       │
23│  - Market data parsing                               │
24│  - Order execution                                   │
25│  - Pricing algorithms                                │
26└──────────────────────────────────────────────────────┘
27

Key Lessons Learned#

  1. Use C as the Interface Layer: C ABI is stable; C++ name mangling is not
  2. Minimize FFI Crossings: Each call has overhead; batch operations when possible
  3. Memory Management is Critical: Be explicit about ownership across boundaries
  4. PyBind11 > ctypes for Performance: Native NumPy integration is much faster
  5. Rust FFI is Zero Cost: When done right, no overhead vs pure Rust
  6. Test Error Paths: FFI failures are the hardest to debug
  7. Version Everything: ABI compatibility breaks are subtle and painful
  8. Profile Before Optimizing: Sometimes pure Python is fast enough

Conclusion#

Multi-language systems let you use the right tool for each job. The key is clean interfaces, careful memory management, and understanding the trade-offs. When done well, you get Python's productivity, Rust's safety, OCaml's type system, and C++'s performance—all in one system.

The FFI overhead is negligible when you batch operations and design interfaces carefully. The real cost is complexity, so only cross language boundaries when the benefits are clear.


Building multi-language systems? Contact us to discuss architecture and implementation strategies.

NT

NordVarg Team

Technical Writer

NordVarg Team is a software engineer at NordVarg specializing in high-performance financial systems and type-safe programming.

RustOCamlPythonC++FFI

Join 1,000+ Engineers

Get weekly insights on building high-performance financial systems, latest industry trends, and expert tips delivered straight to your inbox.

✓Weekly articles
✓Industry insights
✓No spam, ever

Related Posts

Nov 11, 2025•7 min read
Practical C++ for Sub‑Microsecond Latency: Micro‑Optimizations That Actually Matter
PerformanceC++Low-Latency
Nov 11, 2025•7 min read
CPU Internals for Software Engineers: Caches, Pipelines, and the Cost of a Branch
PerformanceCPUArchitecture
Oct 22, 2024•14 min read
Building a High-Frequency Market Data Feed: Architecture and Optimization
Designing and implementing ultra-low latency market data feeds that process millions of messages per second with microsecond precision
PerformanceMarket DataLow Latency

Interested in working together?