Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SQLite.Interop.dll for ARM32 Windows (ARMNT / Thumb-2)

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 problem this actually solves

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.

Recovering the mapping — the reusable part

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.

The correction that makes it right

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.


Known deviation: no encryption

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.


Reproducibility

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.


Scope — tested and not tested

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 (net45 and net46 builds, 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_rekey are stubs returning SQLITE_ERROR (see above)
  • Other System.Data.SQLite versions — the mangled names change between versions; the recovery method carries over, the generated .def does 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

Toolchain — pin these exactly

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> `
    -Verify

No 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).


Gates

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.


Layout

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

Licence

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.

About

Build System.Data.SQLite's native SQLite.Interop.dll for 32-bit ARM Windows (ARMNT), and recover the mangled SI<hash> entry points that otherwise cause EntryPointNotFoundException

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages