Linux shared libraries that let Space Engineers 1 and Space Engineers 2 load
their Windows native DLLs. Most wrappers are C++17 shims around a custom PE
(Portable Executable) loader. They load the original DLL, translate calls
between the System V and Microsoft x64 ABIs, and implement the Win32 and CRT
functions those DLLs need. Events and semaphores use ntsync when /dev/ntsync
is available, with Linux synchronization primitives as the fallback.
PlatformWindows is an exception. It is a native presenter adapter and does not load a Windows DLL.
| Output | Game | Purpose |
|---|---|---|
libHavok.so |
SE1 | Loads Havok.dll for physics |
libRecastDetour.so |
SE1 | Loads RecastDetour.dll for navmesh |
libVRageNative.so |
SE1 | Loads VRage.Native.dll for voxels |
libD3DCompiler.so |
SE1 | Loads d3dcompiler_47.dll for shader compilation |
libVRage.KytheraV2.Native.so |
SE2 | Loads VRage.KytheraV2.Native.dll and its Kythera_*.dll imports for AI navigation |
libVRage.Platform.Windows.Native.so |
SE2 | Provides the native presenter adapter |
libVRage.Slug.Native.so |
SE2 | Loads VRage.Slug.Native.dll for text rendering |
libVRage.Physics.Native.so |
SE2 | Loads VRage.Physics.Native.dll for physics |
libVRage.Voxels.Native.so |
SE2 | Loads VRage.Voxels.Native.dll for voxels |
CMake also builds the generate_pe_sidecar and d3dcompiler_tool command-line
tools. Release archives contain only the shared libraries in the table.
Loader-backed wrappers expose Init(dllPath, sidecarPath). The first argument
is the path to the matching Windows DLL. The second is an optional cache path
for its ELF sidecar; the parent directory must be writable when the sidecar
needs to be created or replaced. Call Init before using the wrapper's other
exports. Physics, Slug, Voxels, and KytheraV2 enforce this explicitly. Wrappers
do not search for game installations or DLLs; the only other DLLs they load are
PE imports found next to the DLL passed to Init, described below.
This loader implements the Windows APIs used by these DLLs. It is not a general
PE or Win32 runtime. It supports x86-64 PE32+ images and selected imports.
A PE import that is not a shimmed module and sits next to the importing DLL is
loaded from there, linked and attached first, and gets its own sidecar next to
the importer's one. KytheraV2 loads 18 Kythera_*.dll files this way.
Unsupported imports use trap stubs, MSVC exception boundaries abort the
process, and loaded PE images cannot be unloaded safely. These failures are
intentional: continuing would leave the process in an unknown state.
The Havok, Physics, Voxels, and KytheraV2 wrappers are generated from decompiled
C# DllImport declarations. Their generated C++ files are committed, so
building the project does not require decompiled sources.
Each generator takes one decompiled source root. The root may contain the
assembly directly, under src/, or be the assembly directory itself.
python3 tools/generate_havok_wrapper.py /path/to/se1/decompiled
python3 tools/generate_physics_wrapper.py /path/to/se2/decompiled
python3 tools/generate_voxels_wrapper.py /path/to/se2/decompiled
python3 tools/generate_kythera_wrapper.py /path/to/se2/decompiledThese commands rewrite src/Havok.cpp, src/Physics.cpp,
src/Physics.exports, src/Voxels.cpp, and src/KytheraV2.cpp as applicable.
Run the generator smoke tests after regeneration:
ctest --test-dir build --output-on-failureHavok accepts Microsoft-ABI callback pointers, while managed delegates use the System V ABI on Linux. The generated wrapper uses one fixed bridge for each static semantic callback, serialized bridges for the two callbacks that live only for a native call, and three shared bridges for phantom shapes. Phantom callbacks are routed through a map keyed by the native shape pointer.
KytV2_CreateCore receives the managed callback table. The wrapper passes the
bridge its own long-lived copy, with an ms_abi bridge to the managed
RequestBackgroundWork function and its argument unchanged. The two profiler
callbacks are Optick exports, which have no Linux build, so the copy gets
no-op ms_abi stubs in their place. Kythera returns navmesh triangles, paths and cover points
in CoTaskMemAlloc buffers. The shim allocates them with malloc, because .NET
frees them with free through Marshal.FreeCoTaskMem.
Some MSVC-decorated Physics and Slug exports contain @, which GNU ELF tools
interpret as symbol-version syntax. The source uses equal-length $
placeholders. Post-build scripts replace those names in the ELF dynamic string
table and rebuild its System V symbol hash.
Physics records exact placeholder-to-export mappings in
src/Physics.exports. Slug has 14 known exports and derives each final name by
replacing $ with @, so it does not need a manifest. CMake runs both renamers
automatically and fails the build if the expected symbols are not found.
Both targets also compile with -fno-reorder-blocks-and-partition. At -O2
and above GCC otherwise splits cold paths into a separate section and appends
.cold to the mangled symbol name, producing identifiers the GNU assembler
cannot parse.
The direct CMake build requires x86-64 Linux, CMake 3.13 or newer, Python 3,
and a C++17-capable g++. The supplied Makefile uses a CMake preset and
therefore requires CMake 3.21 or newer. The packaging script also uses Bash,
GNU Make, GNU tar, GNU coreutils, and GNU binutils (readelf, objcopy, strip).
make
ls build/*.so
make cleanmake configures build/ with the default preset, builds every target, and
does not select a CMake build type. Use a direct CMake configuration when you
need a specific build type:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j "$(nproc)"
ctest --test-dir build --output-on-failureCTest is enabled by default. Pass -DBUILD_TESTING=OFF when only the libraries
and command-line tools are needed.
Every target is compiled with -mtls-dialect=gnu2. The Win32 and CRT shims are
ms_abi functions, and GCC 12 and 13 assume the traditional
call __tls_get_addr sequence preserves RDI and RSI inside them, which it does
not. In an optimized shared library that turned a thread_local access in a
shim into a call with a corrupted argument; the CRT qsort bridge sorted the
GOT instead of the caller's array, and CreateEventW never ran the ntsync
initializer. TLS descriptors resolve thread-local addresses with a call that
preserves every register except RAX, so the shims are safe regardless of what
the compiler keeps in RDI or RSI. Debug builds at -O0 were never affected.
The tls_dialect test fails if a library imports __tls_get_addr again.
A normal build has these self-contained tests:
| Test | Coverage |
|---|---|
se1_generators |
SE1 generator callback patterns and committed source markers |
se2_generators |
SE2 generators with synthetic C# declarations |
package_symbols |
Sample ELF: Release symbol split, shipped hashes/build IDs, patched exports, behavior and reproducibility |
thread_id_dll_attach |
PE thread attach, detach, TLS, and FLS lifetime |
windows_memory |
Windows memory and runtime behavior |
crt_qsort |
Shared-library ABI bridge; repeated, nested, concurrent and empty/single-item sorts |
tls_dialect |
No wrapper library imports __tls_get_addr (see above) |
ntsync_init_event, ntsync_init_semaphore |
Each API initializes ntsync independently in a fresh process and completes a shared-library round trip when the device is accessible |
ms_abi_tls |
An ms_abi function in a shared library keeps register values across a thread-local access, with dynamic TLS resolution forced |
No C++ test compiles the wrapper sources into its executable; they link or load shared
libraries instead. In an executable the compiler uses static TLS models, so a
miscompiled dynamic TLS access in the libraries would never show up.
thread_id_dll_attach needs the test-only hooks behind NATIVE_WRAPPERS_TESTING; it
links build/tests/libNativeWrappersTesting.so, the shared sources built as a library
with those hooks. The release packaging and the build/*.so deploy globs skip it.
With BIN64 pointing at the game's Bin64, the Havok tests also run against the real
Havok.dll:
| Test | Coverage |
|---|---|
havok_unwind |
Crash backtraces name PE frames and the wrapper export |
havok_memory |
Repeated shape creation does not leak Havok memory |
havok_endofstep_detects |
The havok_endofstep_test oracle finds the dangling contact manager that crashes SE1 inside FinishMtStep (SE1-0015); expected to report a violation |
havok_endofstep |
The same stress run stays clean with --fix, the Bugfixes plugin's workaround ported into the harness |
havok_endofstep_test is also a standalone stress harness: it steps several worlds the way
MyPhysics.StepWorldsParallel does, churns bodies with the game's lifetime sequences and
checks the end-of-step callback list before every FinishMtStep. See the usage comment in
tests/havok_endofstep.cpp for the scenarios and tests/havok_endofstep_loop.sh for a
seeded soak. The game gets the fix from the Bugfixes plugin (HavokEndOfStepCallbackPatch),
not from the wrappers; keep the harness copy of it in sync. If a future Havok.dll fixes the
bug, havok_endofstep_detects starts failing, which is the signal to retire the plugin patch.
Run them with:
ctest --test-dir build --output-on-failureCTest runs ms_abi_tls with
GLIBC_TUNABLES=glibc.rtld.optional_static_tls=0. Without this setting, glibc
can allocate the small probe in static TLS, so fresh threads alone do not
exercise the dynamic resolver. Use the same environment setting when running
ms_abi_tls_test directly.
The generator tests are smoke tests. They do not compare a full regeneration from the game assemblies with every committed wrapper.
Run the packaging check independently with python3 tests/package_symbols.py.
It compiles a tiny sample library and needs GNU binutils; it builds no wrappers.
build_and_package.sh builds the project, runs CTest, and packages the wrappers
in both configurations. It produces the six archives used by the release
workflow:
./build_and_package.sh
ls dist/*.tar.gzSet BUILD_JOBS=4 to bound compiler concurrency on memory-constrained machines.
Packaging verifies all six archives and writes dist/release-manifest.json with
their exact SHA-256 hashes and source commit. Repeat the check independently with
python3 tools/verify_release.py dist; it reads ELF metadata without loading wrappers.
It builds and packages Release first, then Debug, starting each configuration
from make clean. The archives land in dist/, which is excluded from Git,
and build/ is left holding the Debug libraries, so local development gets
unoptimized binaries with full symbols and usable backtraces without another
build. The build workflow runs the same script, so local archives have the same
names and layout as published archives. Binary contents still depend on the
local compiler and system libraries.
Loader-backed wrappers can write an ELF sidecar to the cache path supplied by their caller. The sidecar keeps exported names, synthetic names for runtime functions, and supported unwind metadata available to Linux crash tools after the process exits. It does not contain PDB or private symbols.
Create a persistent sidecar directly when a saved core or another tool needs to reopen it later:
build/generate_pe_sidecar /path/to/Havok.dll /path/to/cache/Havok.dllThe loader checks the sidecar version, source size, and full-file FNV-1a fingerprint before using its mapped image. It atomically replaces a stale sidecar. If the cache path cannot provide a usable sidecar, the loader falls back to the raw PE image. Sidecar generation supports x86-64 PE32+ images and a subset of Windows x64 unwind records.
The optional tests below use proprietary game DLLs. CI does not provide those DLLs, so it skips these tests. Set one or both directory options while configuring the build:
cmake -S . -B build \
-DBIN64=/path/to/SpaceEngineers/Bin64 \
-DGAME2=/path/to/SpaceEngineers2/Game2
cmake --build build
ctest --test-dir build --output-on-failure| Option and required file | Added tests |
|---|---|
BIN64/d3dcompiler_47.dll |
d3dcompiler_preprocess, d3dcompiler_diagnostics, d3dcompiler_forward_include |
BIN64/RecastDetour.dll |
recast_detour |
BIN64/Havok.dll |
pe_sidecar_generation, havok_unwind, havok_memory |
GAME2/VRage.Physics.Native.dll |
physics_init |
GAME2/VRage.Voxels.Native.dll and VRage.Slug.Native.dll |
voxels_teb |
GAME2/VRage.KytheraV2.Native.dll |
kythera_lifecycle |
voxels_teb calls the Voxels planet shape, then Slug, then the planet shape
again on one thread. The second Voxels call must set up its own TEB again,
because the planet shape reads a thread-local variable of the DLL.
kythera_lifecycle runs the startup, update and teardown calls of
KytheraCore, builds a ground navmesh on a test quad and queries a path on it,
then updates a surface agent while GS points at another TEB, as it does after a
Physics or Slug call on the same thread. Navmesh tiles build on background
threads that call KytV2_DoBackgroundWork when the bridge asks for work. A
last thread exits with GS on a foreign TEB, which the bridge's thread detach
callbacks must survive. It fails on any unknown or ordinal import.
The Havok crash harness can also be run directly:
build/havok_crash_test /path/to/SpaceEngineers/Bin64/Havok.dll --trace
gdb --args build/havok_crash_test /path/to/SpaceEngineers/Bin64/Havok.dllThe --trace form checks the unwind trace and exits normally. Without
--trace, the harness leaves the crash unhandled for GDB or core testing.
The build workflow runs for pushes to main. It
also runs when a non-draft pull request is opened, updated, reopened, or marked
ready for review. The workflow invokes
build_and_package.sh, which builds and tests the
Release and Debug configurations and packages the shared libraries at the root
of one archive per game and configuration. Each archive holds only the wrappers
its game version loads, as listed in Libraries built:
| Asset | Game | Configuration |
|---|---|---|
se1-native-wrappers.tar.gz |
SE1 | Release (-O3 -DNDEBUG) |
se2-native-wrappers.tar.gz |
SE2 | Release (-O3 -DNDEBUG) |
se1-native-wrappers.debug.tar.gz |
SE1 | Debug (-O0 -g) |
se2-native-wrappers.debug.tar.gz |
SE2 | Debug (-O0 -g) |
se1-native-wrappers.symbols.tar.gz |
SE1 | Split symbols matching the optimized Release libraries |
se2-native-wrappers.symbols.tar.gz |
SE2 | Split symbols matching the optimized Release libraries |
A push to main publishes a public v1.0.<run> release and marks it as the
latest release. For a non-draft pull request, the workflow maintains a draft
release with the tag pr-<number>. Draft publication requires repository write
permission, which fork pull requests do not receive from the standard token.
Pulsar for Linux and Magnetar consume se1-native-wrappers.tar.gz by asset
name. The Debug archives retain symbols for debugging. These per-game archives
replaced the single combined linux-native-wrappers.tar.gz, which releases no
longer carry.
Release compilation retains -O3 -DNDEBUG and adds -g plus an ELF build ID.
Packaging copies the libraries to staging, extracts DWARF into .so.debug files,
then runs strip --strip-debug and adds a GNU debuglink to each staged library.
Runtime downloads contain no DWARF. These operations change ELF file bytes while
preserving executable code and runtime behavior; they do not strip the original
compiler outputs in build/. The packaging script subsequently cleans that
directory before its existing Debug build.
Each symbols archive contains the matching .so.debug files and symbols.json.
The manifest records the source commit, ELF build IDs, symbol-file SHA-256 hashes,
and SHA-256 hashes of the shipped libraries after stripping and debuglink.
Compare the runtime library using sha256sum libHavok.so and
readelf -n libHavok.so before selecting symbols. Extract its exact release's
symbols archive alongside the runtime libraries (or put the .debug files in
their .debug/ subdirectory) so GDB can follow the GNU debuglinks.
Retain the exact runtime and symbols assets together for every release under
investigation. A separately compiled Debug (-O0) archive cannot symbolize the
optimized Release code, and rebuilding the same source later does not guarantee
matching binaries. These assets enable symbolication for new releases; they do
not recover DWARF omitted from older releases. Loaded Windows DLLs still require
their own matching symbols; wrapper DWARF does not supply those.
The project is MIT licensed; see LICENSE. The bundled Linux ntsync
UAPI header in src/linux/ntsync.h carries its own
GPL-2.0 WITH Linux-syscall-note SPDX identifier.