So I had a placeholder buoyancy script in pure C#, and now I need to replace it with a C++ DLL. The goal is simple, C++ does the heavy math, C# just calls into it and applies the result. But getting them talking requires both sides to agree on types, memory layout, and calling conventions. Lots of room for "wait, why isn't this working?"
The DLL lives on as three functions, initialize an object with its mesh (called once), compute buoyancy forces (called every physics frame), and clean up. The C# side declares these functions so the runtime can P/Invoke into them at runtime. On the C++ side, each object gets its own handle (just an incrementing integer) and the DLL keeps a map of state vertex positions, triangle indices, center of mass, all copied in and owned by the DLL. This survives across frames. The C# side (NativeBridge.cs) is thin, just a [DllImport("BuoyancyDLL")] attribute followed by extern method declarations. Tell the runtime where the DLL is, declare the signature, C# handles the marshalling.
For a first pass I hardcoded the force to 100N upward. Enough to prove the data round-trips without crashing. This can be "seen" in the picture below where the cube floats up when it has a weight of 10kg.

I spent most of the day hitting interop gotchas. Some of them were predictable, some were just... bad assumptions on my part. C# doesn't have const parameters, I copied the C++ signature directly, const float[] vertices, not realizing C# doesn't let you mark parameters as const. It's only for compile-time constants. The C++ const is a promise not to modify the data, the marshaller doesn't need to know about it. Just use float[].
DllImport paths are weird, I tried [DllImport("../Plugins/x86_64/BuoyancyDLL.dll")] thinking I was being specific. Nope. Unity looks for DLLs by name in the Plugins folder, so just "BuoyancyDLL" works. Relative paths also hardcode the build config which is fragile.
Header and source disagreed, I added a parameter (algoChoice) to the C# side and the .cpp, but forgot to update the header file. The compiler didn't catch this, each .cpp compiles independently. The linker would have exploded later if I'd actually tried to link, but I didn't notice until testing.
Struct syntax errors, C++ struct members need semicolons, not commas. I wrote an entire struct with commas and didn't notice until the build failed. Missing includes, memcpy lives in <cstring>. Visual Studio sometimes includes it transitively through other headers, but I can't rely on that.
Byte math on arrays, memcpy copies bytes, not elements. I passed indexCount when I should've passed indexCount * sizeof(int). This copies a quarter of the index data. Triangles came out garbled until I figured it out.
how it works
On Start() I grab the mesh from MeshFilter, flatten Unity's Vector3[] into a float[] (C++ won't understand Unity types), and send it along with the index array and center of mass. The DLL copies everything into its ObjectData, increments the handle counter, and returns the handle.
On FixedUpdate() I build the model-to-world matrix (16 floats, column-major), send it with the handle, and the DLL writes back a float[6], three for force, three for torque, C# reads that and then applies the forces.
One thing that matters, Unity's Matrix4x4 and GLM both default to column-major layout. The memory layout matches exactly, so no conversion needed. Just loop and copy.
testing
Rebuilt from Developer PowerShell, hit Play in Unity. The cube floats up, not because of any real buoyancy math, just that hardcoded 100N, but it proves the entire chain works:
BuoyancyController -> NativeBridge -> P/Invoke -> DLL -> force back -> Rigidbody
Next is the actual buoyancy simulation using Hirae's closed-form surface pressure integral. That's the interesting part :D