Define an explicit interoperability contract
The trade-off. More explicit marshalling and contract work, in exchange for a smaller, inspectable compatibility surface.
Independent case study / Crown Jewels Bridge
A controlled C++/.NET interoperability study examining interface contracts, resource ownership and numerical compatibility. The design separates boundary hardening from changes to the underlying pricing logic.
Scope. This is a controlled Windows/MSVC/.NET experiment around a deliberately constrained core. It does not establish customer cutover success, financial certification, Azure deployment reliability or production-wide safety.
01 / Problem & constraints
Modernization introduces compatibility risks beyond the pricing formula. Input validation, memory ownership, marshalling and compiler settings can each change observable behavior.
This self-directed study separates those risks. It compares hardened and legacy boundaries around the same computational module, then introduces a fast-floating-point build to isolate compiler effects. It is not a customer migration.
Define and evaluate the integration contract while preserving the computational core.
02 / Architecture
Bridge.Core exposes the managed calling surface and SafeHandle-based resource ownership.
A narrow native contract defines layouts, capacities, input validation and clean failure behavior.
The core module is reused across builds; boundary and compiler changes remain separate variables.
Native checks, managed tests and generated reports compare unsafe outcomes and numerical differences.
The interface is also a risk-control point: host modernization and pricing-semantics changes are evaluated as separate concerns.
03 / Design decisions
The trade-off. More explicit marshalling and contract work, in exchange for a smaller, inspectable compatibility surface.
The trade-off. Assembly-wide DisableRuntimeMarshalling constraints do not fit both paths; separation makes lifetime semantics explicit.
The trade-off. Extra build and comparison work is necessary to distinguish seam defects from compiler-induced drift.
04 / Technical ownership
Independent project ownership: design, implementation and evaluation. My professional cross-team responsibilities are described separately in Technical leadership.
Make layout, buffer capacity, ownership and error semantics explicit so another engineer can review the boundary without reverse-engineering the host.
Inspect the ABI contractRecord the reasons for the wrapper split and boundary strategy rather than leaving them implicit in code.
Read the decision recordsConnect the native and managed checks, report generation and deterministic comparison in the repeatable local workflow.
Inspect the delivery workflow05 / Results & interpretation
Observed in the documented hostile-input experiment, not a rate measured in customer traffic.
For that same sampled input set. This is bounded evidence, not proof of safety for every input.
A separate compiler comparison changed 2,818 of 4,000 positions despite the unchanged engine module. The design lesson is to qualify the complete build and calling contract, not just the source diff.
06 / Implementation & documentation
Where validation and failure semantics meet the native engine.
native/src/abi.cppThe wrapper contract and lifetime behavior, including ownership-sensitive cases.
Bridge.TestsCommitted deterministic evidence, with the full experiment explained in the README.
docs/results-stable.mdEnvironment requirements and limits that matter before any production adaptation.
docs/known-limitations.mdA 60,000-line C++ pricing engine that nobody is allowed to change, called safely from .NET 10 -- by changing nothing but the 300 lines it is reached through.
C++20 / C# / .NET 10 / MSVC. 177 managed tests, 129 native checks, 6 mutation kills.
Three DLLs from byte-identical engine.cpp.
Every modernisation programme meets this building eventually. There is a library at the centre of the business that was written by people who left, in a language the current team does not use, with no test suite, and it is right -- it has priced the book correctly for fifteen years and the desk trusts it. The mandate is to modernise everything around it and not to touch it.
That mandate is usually treated as an obstacle. It is actually the correct engineering decision, and this project is the argument for why: the danger was never in the 60,000 lines. It was in the 300 lines they are reached through.
To make that argument rather than assert it, the repository builds the same C++ engine into three DLLs that differ only in preprocessor definitions:
| DLL | boundary (abi.cpp) |
floating point |
|---|---|---|
pricing.dll |
hardened: validates, bounds, catches | /fp:precise |
pricing_legacy.dll |
as found in 2009: trusts everything | /fp:precise |
pricing_fast.dll |
hardened | /fp:fast /arch:AVX2 |
engine.cpp is byte-identical in all three. Every difference measured below therefore
belongs to the boundary or to the compiler, and never to somebody quietly improving the
maths while nobody was looking -- which is the failure mode that makes this kind of
comparison worthless in practice.
You do not have to audit the C++. You have to own the boundary.
Six hundred hostile inputs through the 2009 boundary produce 328 unsafe outcomes.
The same corpus through the hardened boundary produces zero, with engine.cpp
untouched.
docs/results.md is the evidence. It is generated by the code, it writes down twelve
predictions before it measures anything, and eleven of them turned out to be wrong.
Crashing is the safe failure. Of the 328 unsafe outcomes against the 2009 boundary,
58 killed the process and 263 returned a wrong answer with a success code. The
crashes get noticed within the hour. A negative option price returned as Ok gets
booked, settles, and is discovered in a reconciliation three weeks later by someone who
has no idea where to start looking. The taxonomy in this project is ordered
Accepted < RejectedCleanly < SilentlyWrong < MemoryCorruption < ProcessDied, and
SilentlyWrong sitting above MemoryCorruption is deliberate.
A sign error in a volatility feed prices the opposite instrument, exactly. In the
Black-Scholes d1, sigma appears squared in the numerator and once in the denominator, so
negating it flips the sign of both d1 and d2 -- which turns out to give
call(-sigma) == -put(sigma) as an identity, verified here to twelve decimal places
against the legacy DLL. The result is not a NaN and not an obviously silly number. It is
a small, finite, plausible, bookable price for a different contract. No validation
anywhere downstream can catch that, because there is nothing wrong with the number.
A short output buffer does not crash. That is the problem. Handing the batch entry
point sixty-four options and a buffer with room for one writes exactly sixty-three
doubles past the end and returns success. It does not fault, because 512 bytes lands
inside allocator padding. A test that waits for an access violation reports this input as
safe. Detecting it requires writing a known pattern past the buffer and counting how much
of it survived -- which is how the number sixty-three is an assertion in this repository
rather than an anecdote. The quieter sibling: a lattice with zero steps returns 0.0
with a success code for an American put worth 91 cents, and zero is exactly what an
out-of-the-money put is supposed to look like.
The compiler flags change the price. Recompiling the untouched engine with
/fp:fast /arch:AVX2 changes 2,818 of 4,000 positions, by up to 602 ULP. The magnitudes
are far below anything a desk would notice on one trade, and that is what makes them
expensive: a modernisation programme that recompiles for speed and runs the old and new
systems in parallel will see a steady drip of tiny unexplained breaks, conclude they are
noise, and lose the ability to distinguish noise from a real regression.
native/
include/pricing_abi.h the C ABI: 9 entry points, static_asserts on every offset
src/engine.cpp the crown jewels. Byte-identical in all three DLLs.
src/abi.cpp the boundary. #if PJ_HARDENED is the entire experiment.
tests/selftest.cpp 129 checks with no CLR in the process at all
Bridge.Core/ the safe host: SafeHandle, offset verification, one strategy
Bridge.Legacy/ raw access to all three DLLs, for measuring
Bridge.Report/ the experiment harness; writes docs/results*.md
Bridge.Tests/ 177 tests
Bridge.Core and Bridge.Legacy are split because of one line:
<DisableRuntimeMarshalling>true</DisableRuntimeMarshalling>
It is assembly-wide, and it disables SafeHandle marshalling along with everything else.
So the assembly that wants a SafeHandle cannot have it, and every call site in
Bridge.Core hand-writes DangerousAddRef / DangerousGetHandle / DangerousRelease
around the P/Invoke. The split is not tidiness; it is the only way to have both the fast
calling convention and a handle that cannot be used after it is freed.
.\build.ps1 # three DLLs + selftest via vcvars/CMake/Ninja, then the managed solution
.\test.ps1 # 7 stages: distinct binaries, native selftest, build, tests,
# report regeneration, mutation testing, secret scan
.\demo.ps1 # the three findings above, in about ninety seconds
test.ps1 regenerates docs/results-stable.md and byte-compares it against the
committed copy. A project whose entire output is a set of measurements cannot ship
measurements nobody re-ran.
Requires MSVC with C++20, CMake, Ninja, and the .NET 10 SDK. Windows only, by construction -- the subject is a Windows-hosted C++ library and its calling convention.
The tests found three real defects in my own hardened boundary, and the most interesting one was not a safety hole:
It refused the most routine input in the book. Volatility of zero and time-to-expiry
of zero were both rejected as out of domain. But every option expiring today has
years == 0, on every expiry date, for every strike. A boundary whose safety comes from
refusing legal input is not safe, it is unusable, and it will be switched off by the
first person who has a deadline. The hardened build now takes the limit at the seam and
returns the correct discounted intrinsic for inputs the 2009 core turns into NaN -- so it
is more correct than the original, not merely stricter. Greeks still refuse, explicitly,
because theta is unbounded at expiry and gamma at zero volatility is a delta function.
Refusing an answer that does not exist is safety. Refusing one that does is breakage.
There was also a double-free in the harness itself, which is documented in
Bridge.Tests/EngineLifetimeTests.cs because its shape is the shape of every real one:
nobody wrote Free(h); Free(h);. There was a using in the caller and a second type
that also believed it owned the module. Both were individually correct.
docs/known-limitations.md lists what this does not do, including the two experiments
that were designed and abandoned because they could not be made to mean anything.
docs/results.md -- the full generated report, twelve predictions, timings includeddocs/results-stable.md -- the reproducible subset, byte-compared by test.ps1docs/adr/ -- five decisions, including why engine.cpp was never allowed to changedocs/known-limitations.mddocs/security-review.mddocs/portfolio/ -- the long-form write-up07 / Limits & production considerations
This is a controlled Windows/MSVC/.NET experiment around a deliberately constrained core. It does not establish customer cutover success, financial certification, Azure deployment reliability or production-wide safety.
Before a real deployment I would require domain-approved pricing fixtures, workload-specific compatibility checks, traffic shadowing and a staged rollback plan. These are proposed production steps, not completed project outcomes.