LibLoad Environment

lwIP-CE is too big to copy into every program that wants to use it. The core plus TLS is around 200 KB, and the calculator only has just under 2 MB of Flash, so statically linking it into each consumer would burn that space fast. Instead, lwIP-CE ships as a resident application that other programs call into. One copy lives on the calculator; everything else dispatches to it.

That “call into a resident app” trick is built on top of the CE toolchain’s LIBLOAD mechanism – but with some custom logic bolted on, because LIBLOAD was not designed for this exact use case. This page explains how the two fit together.

What LIBLOAD normally does

LIBLOAD is the toolchain’s way of sharing code between programs without recompiling it into each one. The usual flow looks like this:

  • A library exposes a list of exported functions (its public API).

  • At link time, those exports become a jump table – one trampoline per function, each holding an offset rather than a real address.

  • When a consumer program starts, a LIBLOAD bootstrap finds the library in memory and rewrites each trampoline: real address = library base + offset.

  • After that, calling an API function just hits its trampoline, which jumps to the resolved address.

This works great for small libraries that live in RAM as AppVars. The catch: LIBLOAD assumes the library is the thing being loaded. lwIP-CE is an Application sitting in Flash, not a RAM AppVar, so it needs a bit more.

What lwIP-CE adds on top

Two pieces make an Application usable as a LIBLOAD-style API source:

  • The lwIP app links its own export table into the Flash image (the same idea as a library’s export table). The linker pins it at a fixed early offset from the app’s linked image base, and the bootstrap computes that linked image base from the installed app metadata at runtime.

  • A small companion library (the LIBLOAD stub the consumer actually links against) provides the consumer-side trampolines plus a bootstrap function. The bootstrap locates the installed lwIP app, reads its export table, and patches the local trampolines to point into the resident app.

So the consumer links against the tiny stub; the stub, once bootstrapped, routes every lwip_* call into the real app in Flash.

The init handshake

Bringing the stack up happens in two stages – the normal LIBLOAD part, then lwIP-CE’s own part.

Stage 1 – LIBLOAD does its usual thing. When the consumer program loads, LIBLOAD initializes the companion library exactly like any other library. The library also pulls in usbdrvce via include_library, so its imports table gets the USB driver function pointers filled in at this point. The lwIP export trampolines exist but are not yet patched – calling one now would go nowhere.

Stage 2 – the consumer calls lwip_init_runtime(malloc, free, realloc). This is where the custom logic runs:

  1. The three C-runtime pointers (malloc, free, realloc) are stored into the imports table. lwIP-CE has no libc of its own; it borrows the caller’s.

  2. The bootstrap locates the resident lwIP application by name and finds its base address.

  3. It reads the installed app metadata to compute the linked image base, adds the fixed dylib descriptor offset, then checks a 6-byte magic marker ("LWIPTB") and the export count to make sure it found a real, compatible table and not a stale or mismatched build.

  4. It walks the export table and patches every consumer-side trampoline: each table entry has already been relocated by the installer to a real in-app Flash address. After this loop, all lwip_* calls are live and dispatch into the app.

  5. Finally – now that the trampolines work – it calls into the app one more time, to lwip_init_runtime_internal. The app does the startup work that normally happens automatically but doesn’t here (because the app was never “launched” in the usual sense): it zeroes its .bss, copies its .data from Flash into RAM, and copies the imports table (CRT + USB pointers) into its own reserved storage so its internal code can dispatch through them.

After lwip_init_runtime returns successfully, the stack is fully wired: the consumer’s malloc is in place, USB is reachable, and every API call lands in the resident app.

The whole sequence, end to end:

STAGE 1  (ordinary LIBLOAD)
+-------------------------------------------------------------+
| LIBLOAD loads the companion library                         |
| USB vtable filled via include_library 'usbdrvce';           |
| lwIP trampolines present but NOT yet patched                |
+-------------------------------------------------------------+
                           |
                           v
STAGE 2  (lwIP-CE custom setup -- triggered by the consumer)
+-------------------------------------------------------------+
| consumer calls lwip_init_runtime(malloc, free, realloc)     |
| -> host CRT pointers written into the imports table         |
+-------------------------------------------------------------+
                           |
                           v
+-------------------------------------------------------------+
| bootstrap locates resident app and computes linked image    |
| base from app metadata; + fixed descriptor offset -> table  |
+-------------------------------------------------------------+
                           |
                           v
+-------------------------------------------------------------+
| verify "LWIPTB" magic and export count                      |
| mismatch => fail closed (stale / incompatible app)          |
+-------------------------------------------------------------+
                           |
                           v
+-------------------------------------------------------------+
| patch each trampoline -> relocated in-app address           |
| after this loop, every lwip_* call dispatches into the app  |
+-------------------------------------------------------------+
                           |
                           v
+-------------------------------------------------------------+
| call lwip_init_runtime_internal  (now reachable!)           |
| app zeroes .bss, copies .data from Flash,                   |
| copies imports table into its reserved storage              |
+-------------------------------------------------------------+
                           |
                           v
                  stack ready to use

The ordering is the subtle part: lwip_init_runtime_internal is itself an exported call, so it can only run after the trampolines are patched. The bootstrap reaches into the app to finish setting up the app.

Memory layout: the BSSHEAP contract

Because the lwIP app’s .bss and .data get initialized at runtime (Stage 2 above), they need a fixed home in RAM that does not collide with the consumer program’s own variables. lwIP-CE reserves an 8 KiB window starting at the toolchain’s default BSSHEAP_LOW (0xD052C6), running up to 0xD072C6. That window holds the app’s runtime .bss + .data (about 6.6 KiB in practice, with the rest as headroom for API growth).

The practical consequence for consumer programs: you must move your own BSSHEAP_LOW up by 8 KiB so your variables start above lwIP-CE’s reserved window. In other words, link with:

BSSHEAP_LOW >= 0xD072C6

If you leave BSSHEAP_LOW at the default, your program’s BSS and lwIP-CE’s will overlap, and things will corrupt in confusing ways. Bumping it 8 KiB higher is the whole fix.

Pictured (addresses increase upward):

CORRECT: BSSHEAP_LOW raised            WRONG: BSSHEAP_LOW left at default

           +----------------------+
0xD072C6 ->|  consumer BSS / heap |               +----------------------+
           |  (starts here)       |               |  lwIP app window AND |
           +----------------------+    0xD052C6 ->|  consumer BSS BOTH   |
           |  lwIP app            |               |  claim this region   |
           |  .bss + .data (8KiB) |               +----------------------+
0xD052C6 ->|                      |                   overlap => both
           +----------------------+                   regions corrupt

consumer sets BSSHEAP_LOW >= 0xD072C6   consumer left it at 0xD052C6

Why it is done this way

It would be simpler to statically link lwIP into each program – no bootstrap, no trampolines, no memory contract. But “simpler” here means paying ~200 KB of Flash per program that wants networking, on a device that does not have that to spare. The resident-app model trades a one-time init dance for a single shared copy, stable state across callers, and a public interface that does not balloon every consumer’s binary. The handshake above is the price of admission, and it runs once at startup.