A build recipe, not a fork. It produces the native half of
System.Data.SQLite 1.0.116.0 (SQLite 3.38.5) for 32-bit ARM
Windows — an architecture the upstream project has never shipped — so that the stock, unmodified,
strong-name-signed System.Data.SQLite.dll works there.
SQLite.Interop.dll 1,229,824 bytes
SHA256 135A3FEB5EC21EFFD8CD22C6D92144A3E729362C714F44828AC2C31AE96B24EF
Two clean builds of the same source are byte-identical (see Reproducibility).
The obvious approach — build the published source for ARM and ship it — produces a DLL that loads and then fails on every single call.
System.Data.SQLite.dll does not P/Invoke sqlite3_open. It P/Invokes mangled SI<16-hex> names:
[DllImport("SQLite.Interop.dll")] -> entry point "SI13396627163b7740"
Those names appear nowhere in the published source — not in interop.c, not in
UnsafeNativeMethods.cs, not in the .vcxproj / .props / Setup scripts. They are applied by an
unpublished release step. They are deterministic (byte-identical across the x86, x64, net45 and net46
builds) but change between System.Data.SQLite versions, which is the well-known
EntryPointNotFoundException: SI606edd9d386f5df5 class of failure.
The mapping is recoverable authoritatively, from the shipped managed assembly itself. No guessing,
no reverse engineering, and the method applies on any architecture — it is equally useful to anyone
debugging an EntryPointNotFoundException on x86 or x64.
The release step rewrites ImplMap.ImportName (the mangled name) but leaves the C# method name
intact. And these declarations mostly carry no EntryPoint=, so the method name is the intended
entry point. Joining two metadata tables recovers it:
ImplMap.MemberForwarded -> MethodDef.Name (the real symbol)
ImplMap.ImportName (the mangled symbol)
tools/simap.py does exactly that. For 1.0.116.0 it yields 185 pairs — every entry point the
assembly actually calls.
Twelve declarations in UnsafeNativeMethods.cs do carry an explicit EntryPoint=, because C#
needs distinct method names for differently-typed overloads of one native function:
sqlite3_config_none / _int / _log -> sqlite3_config
sqlite3_db_config_charptr / _int_refint / … -> sqlite3_db_config
sqlite3_bind_uint, sqlite3_bind_uint64 -> sqlite3_bind_int, sqlite3_bind_int64
sqlite3_db_filename_bytes -> sqlite3_db_filename
For those, the method name is not the entry point and simap.py alone is wrong.
tools/entrypoints.py extracts the table; tools/gen_def.py applies it as an override.
Of the 12 declared aliases, exactly 2 apply to the 185 names this assembly P/Invokes
(sqlite3_config_none and sqlite3_db_config_charptr); the other 10 are either not in the P/Invoke
set or target a different library (dlopen/dlclose go to libdl/__Internal). Without those two
corrections the link fails on undefined sqlite3_config_none / sqlite3_db_config_charptr — and had
it not failed loudly, the result would have been a DLL with silently wrong aliases.
On injectivity: after the override this particular map happens to remain 1:1 — 185 mangled names
onto 185 distinct targets. Do not rely on that. The alias mechanism exists precisely so several
managed names can share one native function, so a .def generator must not assume injectivity; a
different SDS version can legitimately collapse several mangled names onto one symbol.
13 exported SI names are never P/Invoked by the managed assembly and are deliberately omitted
from the .def. That is a stated omission, not a silent cap.
tools/verify_simap.py cross-checks every recovered name against the shipped DLL's real export table:
185 mapped, 185 exported, 0 missing.
sqlite3_key / sqlite3_rekey (and the _v2 forms) are stubs returning SQLITE_ERROR
(build/stubs.c).
They have to be. SQLite.Interop/src/win/crypt.c is entirely #ifdef SQLITE_HAS_CODEC, and upstream
SQLite removed codec support — in the 3.38.5 amalgamation shipped inside that same source tree,
xCodec, sqlite3PagerSetCodec and SQLITE_HAS_CODEC occur zero times. crypt.c cannot compile
against it.
Yet the official binaries import the full CryptoAPI set (CryptAcquireContextW, CryptDeriveKey,
CryptEncrypt, …). So the official releases are built against a codec-restored amalgamation that is
not published.
Encrypted SQLite databases are therefore not supported by this build. Opening one fails cleanly with
SQLITE_ERROR rather than silently misreading. Applications that never set a Password — the common
case — are unaffected.
Two clean builds produce a byte-identical DLL. /Brepro makes lld derive the PE timestamp from
content instead of the wall clock; without it every build differs in the header and reproducibility is
untestable.
build 1 SHA256 135A3FEB5EC21EFFD8CD22C6D92144A3E729362C714F44828AC2C31AE96B24EF
build 2 SHA256 135A3FEB5EC21EFFD8CD22C6D92144A3E729362C714F44828AC2C31AE96B24EF
RESULT: BYTE-IDENTICAL
This is a contrast with upstream, not a rebuttal of it. Both facts follow from the same cause:
- the official binaries remain irreproducible from public source, because the codec-restored amalgamation they are built from is unpublished;
- this build is reproducible from public source precisely because it omits the codec and stubs
sqlite3_key/sqlite3_rekey.
One fact, two consequences. We can offer verifiability because we cannot offer encryption. Anyone can rebuild from the published source and confirm the binary here matches.
Tested:
- Windows RT 8.1, ARM32 (Surface RT, Tegra 3 / Cortex-A9), .NET Framework 4.8
- Against the stock System.Data.SQLite 1.0.116.0 managed wrapper (
net45andnet46builds, byte-identical to the official NuGet package) - Real workload: Xtreme Download Manager 8.0.29 — plain HTTP and HTTPS downloads, multi-segment transfer, and resume across a deliberate network drop, with the resulting file SHA256-identical to the same download performed on x64
NOT tested:
- Encryption —
sqlite3_key/sqlite3_rekeyare stubs returningSQLITE_ERROR(see above) - Other System.Data.SQLite versions — the mangled names change between versions; the recovery
method carries over, the generated
.defdoes not - EF6 and LINQ providers — only the core ADO.NET surface was exercised
- Windows 10 ARM32 — a different platform from Windows RT 8.1; untried here
| why | ||
|---|---|---|
| clang-cl / lld-link | LLVM 18.1.8 | --target=thumbv7-unknown-windows-msvc |
| MSVC ARM toolset | 14.16.27023 | newer toolsets ship NO arm32 libs |
| Windows SDK | 10.0.19041.0 | needs Lib\…\um\arm + ucrt\arm |
The MSVC pin is the one that bites: Visual Studio dropped the ARM32 (not ARM64) CRT and import libraries, so a current install simply cannot link this. 14.16 is the last usable one.
/MT is deliberate — the output imports kernel32.dll only, so no VC++ redistributable has to be
staged on the target.
.\build\build-arm.ps1 `
-SdsSrc <extracted System.Data.SQLite tree> `
-Llvm <LLVM bin dir> `
-MsvcArm <MSVC toolset with lib\arm> `
-WinSdk <Windows Kits\10> `
-VerifyNo path defaults to anything machine-specific; every one is a parameter or an env var
(SDS_SRC, LLVM_BIN, MSVC_ARM, WINSDK_ROOT, WINSDK_VER).
Verbatim output in gates/GATE-OUTPUT.txt.
| gate | checks | result |
|---|---|---|
| G1 poison | image is ARMNT 0x01C4, no CLR header |
PASS |
| G2 exports | all 185 P/Invoked entry points present | PASS — 0 missing |
| G3 imports | OS DLLs only | PASS — kernel32.dll only |
| G4 Thumb bit | every code export has bit0 set | PASS — 583/583 |
G4 matters because on ARM32 Windows a code pointer with bit0 clear branches into ARM state, which on
Windows RT is a 0xC000001D — and A32 dies intermittently, only under preemption, so it passes every
build check.
The negative control is the point. The same gate against the official x86 build:
export address table entries : 206
in .text, Thumb bit SET : 0
in .text, Thumb bit CLEAR : 206
RESULT: FAIL - 206 even code pointers (ARM-state branch risk)
A gate that cannot fail is not a gate. See INSTRUMENTS.md for the standing rule and
the three times a silently-broken instrument produced a confident wrong answer here.
build/ build-arm.ps1, stubs.c, generated .def, recovered mapping (JSON)
tools/ peclass.py PE + CLI-metadata parser (corflags, TFM, P/Invoke census)
simap.py recovers mangled -> real from ImplMap x MethodDef
entrypoints.py extracts the explicit EntryPoint= overrides
gen_def.py emits the .def
exports.py / importnames.py PE table dumpers
verify_simap.py / verify_arm_interop.py / thumbgate.py the gates
gates/ GATE-OUTPUT.txt verbatim, including the failing negative control
Public domain — see LICENSE. SQLite and System.Data.SQLite are both public domain; no upstream source is vendored here. Unaffiliated with the SQLite project, the System.Data.SQLite maintainers, or Microsoft.