Independent case study / Crown Jewels Bridge

Modernize the interface.Preserve the core.

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.

Contribution
Independent design, implementation and evaluation
Environment
Windows interop experiment
Engineering focus
Contracts / lifetime / compatibility

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.

Managed .NET host, explicit C ABI and unchanged C++ core
Architecture illustration. Validation and ownership live at the seam; the computational module stays unchanged.

01 / Problem & constraints

The question behind the implementation.

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.

Engineering objective

Define and evaluate the integration contract while preserving the computational core.

02 / Architecture

Make the boundaries explicit.

  1. 01

    Managed host

    Bridge.Core exposes the managed calling surface and SafeHandle-based resource ownership.

  2. 02

    Explicit C ABI

    A narrow native contract defines layouts, capacities, input validation and clean failure behavior.

  3. 03

    Unchanged engine

    The core module is reused across builds; boundary and compiler changes remain separate variables.

  4. 04

    Comparison harness

    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

What I chose. What I ruled out.

Define an explicit interoperability contract

Chosen approach
Use a narrow C ABI and validate buffers and inputs before entering the engine.
Alternative considered
Expose native implementation details directly, coupling the managed host to native ownership and layout assumptions.

The trade-off. More explicit marshalling and contract work, in exchange for a smaller, inspectable compatibility surface.

Separate incompatible marshalling requirements

Chosen approach
Keep the safe managed wrapper and the raw comparison layer in separate assemblies.
Alternative considered
Combine both paths under one assembly-wide marshalling configuration.

The trade-off. Assembly-wide DisableRuntimeMarshalling constraints do not fit both paths; separation makes lifetime semantics explicit.

Evaluate compiler settings as behavior changes

Chosen approach
Compare hardened, legacy and fast-floating-point builds using the same engine module.
Alternative considered
Validate source equivalence without separately measuring the effect of compiler settings.

The trade-off. Extra build and comparison work is necessary to distinguish seam defects from compiler-induced drift.

04 / Technical ownership

Turn decisions into reviewable work.

Independent project ownership: design, implementation and evaluation. My professional cross-team responsibilities are described separately in Technical leadership.

01

Define the contract

Make layout, buffer capacity, ownership and error semantics explicit so another engineer can review the boundary without reverse-engineering the host.

Inspect the ABI contract
02

Make decisions reviewable

Record the reasons for the wrapper split and boundary strategy rather than leaving them implicit in code.

Read the decision records
03

Own the delivery evidence

Connect the native and managed checks, report generation and deterministic comparison in the repeatable local workflow.

Inspect the delivery workflow

05 / Results & interpretation

Evidence with its context intact.

328 / 600

Legacy unsafe outcomes

Observed in the documented hostile-input experiment, not a rate measured in customer traffic.

0 / 600

Hardened unsafe outcomes

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

Follow the claim back to its source.

Browse supporting documentation 15 documents / 7 decision records
Read the full project READMEImplementation, setup, evaluation and limitations

Crown Jewels Bridge

A 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.


The problem

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.

The one-sentence finding

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.


Four findings worth the click

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.


What is actually here

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.

Running it

.\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.

What went wrong while building it

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.

Documents

  • docs/results.md -- the full generated report, twelve predictions, timings included
  • docs/results-stable.md -- the reproducible subset, byte-compared by test.ps1
  • docs/adr/ -- five decisions, including why engine.cpp was never allowed to change
  • docs/known-limitations.md
  • docs/security-review.md
  • docs/portfolio/ -- the long-form write-up

07 / Limits & production considerations

Know where the evidence stops.

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 production application

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.