|
| 1 | +# Native Interface ABI Strategy (Revised) |
| 2 | + |
| 3 | +This document describes how the C++ engine and native modules negotiate compatibility, expose new features safely, and validate behavior as the Lua bridge is ported. |
| 4 | + |
| 5 | +## Semantic Versioning |
| 6 | + |
| 7 | +The native interface uses semantic versioning applied to the entire API surface: |
| 8 | + |
| 9 | +- **MAJOR** – incremented when we intentionally break compatibility (removing fields, changing semantics). Modules built against a different major version are rejected. |
| 10 | +- **MINOR** – incremented when we add functionality in a backward-compatible way (new functions, new struct fields appended). Older modules continue to run but cannot see the new entries. |
| 11 | +- **PATCH** – bug fixes or comment-only changes. No behavior change. |
| 12 | + |
| 13 | +Shared header constants: |
| 14 | + |
| 15 | +```c |
| 16 | +#define SPRING_NATIVE_ABI_MAJOR 1 |
| 17 | +#define SPRING_NATIVE_ABI_MINOR 0 |
| 18 | +#define SPRING_NATIVE_ABI_PATCH 0 |
| 19 | +``` |
| 20 | +
|
| 21 | +Rust bindings (via `build.rs` or generated code) read the same numbers so both sides agree. |
| 22 | +
|
| 23 | +## Initialization Handshake |
| 24 | +
|
| 25 | +Compatibility is enforced in a single initialization call. The engine exports: |
| 26 | +
|
| 27 | +```c |
| 28 | +struct NativeInterface; |
| 29 | +
|
| 30 | +struct NativeInitParams { |
| 31 | + uint16_t abi_major; |
| 32 | + uint16_t abi_minor; |
| 33 | + uint16_t abi_patch; |
| 34 | + uint32_t engine_commits_number; // optional, parsed from SpringVersion::GetCommits() |
| 35 | +}; |
| 36 | +
|
| 37 | +typedef bool (*NativeModuleInitFn)(const NativeInterface* iface, |
| 38 | + const NativeInitParams* params, |
| 39 | + struct NativeModuleInfo* out_info); |
| 40 | +``` |
| 41 | + |
| 42 | +Example engine loader: |
| 43 | + |
| 44 | +```c |
| 45 | +bool EngineLoadModule(NativeModuleInitFn initFn) { |
| 46 | + NativeInitParams params; |
| 47 | + params.abi_major = SPRING_NATIVE_ABI_MAJOR; |
| 48 | + params.abi_minor = SPRING_NATIVE_ABI_MINOR; |
| 49 | + params.abi_patch = SPRING_NATIVE_ABI_PATCH; |
| 50 | + params.engine_commits_number = std::atoi(SpringVersion::GetCommits().c_str()); |
| 51 | + |
| 52 | + NativeModuleInfo info = {}; |
| 53 | + if (!initFn(&nativeInterface, ¶ms, &info)) { |
| 54 | + return false; // module rejected the engine |
| 55 | + } |
| 56 | + |
| 57 | + if (info.abi_major != SPRING_NATIVE_ABI_MAJOR) |
| 58 | + return false; |
| 59 | + |
| 60 | + if (info.abi_minor > SPRING_NATIVE_ABI_MINOR) |
| 61 | + return false; |
| 62 | + |
| 63 | + RegisterModule(info); |
| 64 | + return true; |
| 65 | +} |
| 66 | +``` |
| 67 | +- Requires `<cstdlib>` for `std::atoi`. |
| 68 | +
|
| 69 | +The module reports its details through `NativeModuleInfo`: |
| 70 | +
|
| 71 | +```c |
| 72 | +struct NativeModuleInfo { |
| 73 | + const char* module_name; // optional diagnostics |
| 74 | + uint16_t abi_major; |
| 75 | + uint16_t abi_minor; |
| 76 | + uint16_t abi_patch; |
| 77 | +}; |
| 78 | +``` |
| 79 | + |
| 80 | +Once the handshake succeeds both sides know the shared MAJOR/MINOR numbers and can make decisions about optional features. |
| 81 | + |
| 82 | +## Function Tables |
| 83 | + |
| 84 | +Each domain (SyncedCtrl, MetalMap, etc.) exposes a struct of function pointers inside `NativeInterface`. |
| 85 | + |
| 86 | +Rules: |
| 87 | + |
| 88 | +1. New functions are appended to the end of the struct when MINOR increases. |
| 89 | +2. Existing functions keep their signature and semantics until MAJOR changes. |
| 90 | +3. The engine always zero-initialises the function table; missing functions appear as `nullptr`. |
| 91 | +4. Modules must null-check optional entries if they require features added after their own `abi_minor`. |
| 92 | + |
| 93 | +Example skeleton: |
| 94 | + |
| 95 | +```c |
| 96 | +struct NativeSyncedCtrl { |
| 97 | + AddHeightMapFn add_height_map; // since 1.0 |
| 98 | + SetHeightMapFn set_height_map; // since 1.0 |
| 99 | + SetHeightMapFuncFn set_height_map_fn; // since 1.0 |
| 100 | + |
| 101 | + LevelHeightMapFn level_height_map; // added in 1.1 (nullptr on 1.0) |
| 102 | + AdjustHeightMapFn adjust_height_map; // added in 1.1 |
| 103 | +}; |
| 104 | +``` |
| 105 | + |
| 106 | +Usage: |
| 107 | + |
| 108 | +```c |
| 109 | +if (moduleInfo.abi_minor >= 1 && iface->synced_ctrl.level_height_map != nullptr) { |
| 110 | + iface->synced_ctrl.level_height_map(&request); |
| 111 | +} |
| 112 | +``` |
| 113 | + |
| 114 | +Because the engine knows the module’s `abi_minor`, it can also avoid calling back into functions the module never provided. |
| 115 | + |
| 116 | +## Struct Layouts and Arrays |
| 117 | + |
| 118 | +Data exchanged across the boundary stays POD: |
| 119 | + |
| 120 | +- Fixed-width integers (`uint32_t`, `int32_t`, etc.) and `float`/`double`. |
| 121 | +- Pointers combined with explicit lengths for arrays: |
| 122 | + ```c |
| 123 | + struct FloatArrayView { |
| 124 | + const float* values; |
| 125 | + uint32_t length; |
| 126 | + }; |
| 127 | + ``` |
| 128 | +- Null-terminated UTF-8 strings where needed (`const char*`). |
| 129 | +
|
| 130 | +When a struct grows, new fields are appended. The engine only writes fields that exist for the module’s negotiated MINOR version. Array shapes and helper structs live in the specification directory (`spec/README.md`). |
| 131 | +
|
| 132 | +## Call-In / Call-Out Catalogue |
| 133 | +
|
| 134 | +- **Call-ins (module → engine)**: the function tables described above. Modules call what they need, guarding `nullptr` for optional entries. |
| 135 | +- **Call-outs (engine → module)**: planned via callback tables returned in `NativeModuleInfo` when needed. They will follow the same semver rules (append-only while MINOR increases). |
| 136 | +
|
| 137 | +## Hot Reload Workflow |
| 138 | +
|
| 139 | +1. Engine unloads the previous shared library and loads the new one. |
| 140 | +2. `NativeModuleInitFn` runs again; if the module rejects the current ABI, reload aborts. |
| 141 | +3. The module updates its cached pointers to the `NativeInterface` tables. |
| 142 | +
|
| 143 | +
|
| 144 | +## Open Items |
| 145 | +
|
| 146 | +- Decide on the machine-readable format (YAML/TOML/JSON) that will drive code generation. |
| 147 | +- Specify the callback table format once engine-driven call-outs are required. |
0 commit comments