Repository navigation
Merge the .NET (Mono) managed runtime into Meadow.OS - #731
Conversation
Add MONO_RUNTIME_COMMIT, which records the WildernessLabs/runtime commit (meadow-3.0, f980b4c169b) and tree the Mono runtime is built from. The runtime build added in phase 1 will refuse to run against any other checkout. The tree hash is recorded because the runtime branch history has been rewritten once. Run the build workflow for pushes to, and pull requests into, the MergeManagedCode integration branch as well as main, so each phase's pull request is built and uploads its artifacts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Phase 0: pin the Mono runtime and build MergeManagedCode in CI
Add build-mono.sh, which builds the .NET Mono runtime archives against this repository's NuttX headers. It is standalone for now; build.sh and CI will call it once the archives are linked into the OS (phase 3). - The runtime checkout comes from MEADOW_RUNTIME_DIR, or ../runtime beside this repository when that is not set, and must be at the commit and tree recorded in MONO_RUNTIME_COMMIT with no local changes. MEADOW_RUNTIME_ALLOW_UNPINNED=1 relaxes this for runtime development. - The compiler must be arm-none-eabi-gcc 15, the same as the rest of the OS. NuttX must be configured first. - Release by default; --debug builds Debug, --clean starts again and --verbose shows the runtime build output. - Output goes to monoruntime/<release|debug>/ (ignored by git), so several checkouts can build one runtime checkout without clashing. A build stamp records the runtime commit and local changes, the compiler version and a hash of the NuttX configuration; when any of them changes the build directory is discarded and CMake configures again. - The script checks that every archive the OS link needs was produced. Move MONO_RUNTIME_COMMIT to WildernessLabs/runtime Phase1 (a76e66e60c1), which builds cleanly against NuttX 12 with GCC 15. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WildernessLabs/runtime#2 merged Phase1 into the runtime's MergeManagedCode as c668e4a3aea; the tree is unchanged (5cf48371256). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Phase 1: build the Mono runtime against NuttX 12 with build-mono.sh
generate_build_info used to restore user-space.ld from HEAD and then sed the version placeholders into it in place, and restore_versioned_files restored it again on exit. Any uncommitted edit to the linker script was therefore silently discarded by every build. Copy user-space.ld to user-space.stamped.ld (git-ignored), stamp the copy, and link the user image with it (boards/.../kernel/Makefile). The tracked user-space.ld is no longer rewritten or restored. hcom_nuttx_shared.h and the ESP32 version files are still restored from HEAD and stamped in place. Hardware-tested on F7CoreComputeV2 (CCM) with the SDRAM layout change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Mono runtime image is linked to the start of SDRAM and copied there from QSPI before the runtime starts, but the user heap covered all 32 MB from 0xC0000000, so the copy would have overwritten live heap memory. - defconfig: start the user heap after the 3 MB image (CONFIG_HEAP2_BASE 0xc0300000, CONFIG_HEAP2_SIZE 29 MB to the end of SDRAM). This is done by configuration; NuttX's stm32_allocateheap.c is unchanged. - hcom_shared_common.h: add HCOM_NX_MONO_RUNTIME_SDRAM_ADDR (0xC0000000) and make the HCOM_NX_UPD_COPY_RUNTIME_TO_RAM ioctl copy there instead of to CONFIG_HEAP2_BASE. - stm32_boot.c: map the runtime image area for user space (read, write, execute) explicitly. The heap's MPU region covered it only because MPU regions round to 4 MB subregions. - user-space.ld: place the .NET runtime archives in .mono (text), .mono_data and .mono_bss (NOLOAD) after .mono_signature, and discard their unwind tables (R_ARM_PREL31 cannot reach SDRAM from uflash). The sections come first in SECTIONS, so first-match placement replaces the EXCLUDE_FILE lists; the commented-out Mono 6.9 sections are removed. - build.sh: check_mono_sdram_layout cross-checks the image address and size, the memory.ld sdram region, the heap configuration and SDRAM_SIZE, and stops the build if they disagree. Meadow.OS.Runtime.bin is cut from nuttx_user.bin at offsets derived from them (unchanged: 3014400 KB in, 3072 KB long). - hcom: log the user heap size once at startup. - Documentation/Memory.md: describe the SDRAM layout and the raw runtime partition; correct the uflash/kflash table. Hardware-tested on F7CoreComputeV2 (CCM): clean boot, user heap 30,507,004 bytes (3 MB less than before), CLI device info, a 200 KB file write/read/delete round trip, no faults. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Phase 2: reserve the start of SDRAM for the Mono runtime image
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The configuration manager treated a runtime version as packed when any of bits 8-23 of the build field were set, so a build number above 255 (for example 1.99.0.19735) was misdecoded, the board reported "Mono version: (null)" and hcom disabled Mono. Test only the top byte instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Configuration-dependent test symbols (hello_world_results, meadow_kt_dispatcher, tensorflow_tests_hello_world) broke the user-space link when the tests were not built. Make each entry .weak, and move the table from .data to .rodata, which saves 62 KB of user SRAM; the loader only searches it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Mono TLS shim runs in user space and needs meadow_client_cert_retrieve_credentials and meadow_client_cert_release_credentials, which are implemented in the kernel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Phase 3 of the managed runtime plan. apps/system/mono holds the glue ported from Legacy meadow-3.0: the monovm host (mono_main.c), the P/Invoke tables, the mbedTLS 3.6.2 TLS shim and root CA table, and support functions the runtime needs in a protected NuttX 12 build (I-cache maintenance through up_coherent_dcache, an atomic 64-bit compare-and-swap, readlink, mprotect). The runtime archives, libSystem.Native.a and the TLS shim are placed in the SDRAM runtime image; mbedTLS and SQLite stay in user flash. hcom starts meadow_mono_main again. build.sh runs build-mono.sh when CONFIG_SYSTEM_MONO is set. The defconfig enables SYSTEM_MONO, LIBC_LOCALE, FS_RAMMAP (file mmap for assemblies) and mbedTLS threading. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fetch the runtime commit recorded in MONO_RUNTIME_COMMIT into runtime-src/, pass it to the build container as MEADOW_RUNTIME_DIR, and install CMake and Ninja in the build image for the runtime build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WildernessLabs/runtime#3 adds the NuttX 12 fixes the phase 3 glue needs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Phase 3: Mono runtime glue, Kconfig and SDRAM placement
meadow_mono_main ignored the options hcom builds from meadow.config.yaml (MonoControl: Options), so --interp, --debug and the others had no effect. monovm_execute_assembly puts the entry assembly first on the mono_main() command line, where runtime options are no longer read, so join the options into MONO_ENV_OPTIONS, which mono_main() reads first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Turn on CONFIG_HCOM_MONO_STDERR_STDOUT so the Mono app's stdout and stderr reach the host (meadow listen), and fix two faults in hcom_stdxxx_redirect.c that had not been exercised under NuttX 12: - The readers opened the FIFOs O_RDONLY. NuttX reports POLLHUP on every poll of an empty FIFO with no writer, so while Mono was not running the read loop spun and starved hcom (the CLI could not connect). Open them O_RDWR so the reader also counts as a writer. - hcom_mono_open_mono_fifo() ignored its fd argument and dup2'd both FIFOs onto STDOUT_FILENO, so stdout reached the host labelled stderr and stderr was lost. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WildernessLabs/runtime#4: the NuttX 12 stack bounds and the Thumb-2 code buffer alignment. To be moved to the MergeManagedCode merge commit once that pull request is merged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WildernessLabs/runtime#4 is merged; the tree is the same as the Phase4 commit pinned before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WildernessLabs/runtime#5: the 32-bit Thumb-2 LDR/STR encoding for negative offsets, which fixes the interpreter (--interp) crash. To be moved to the MergeManagedCode merge commit once that pull request is merged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WildernessLabs/runtime#5 is merged; the tree is the same as the Phase4b commit pinned before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Phase 4: first Mono boot - Hello World on CCM (JIT and interpreter)
The 3.0 Meadow CLI uses its 3.x deploy path (trimmed publish against the meadow_assemblies package) only when the device reports an OS version of 2.999 or later (MeadowVersion.V3Preview). Mark this build as a 3.0 preview so the CLI's app deploy works against it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
build-managed-assemblies.sh needs the same runtime location and MONO_RUNTIME_COMMIT check as build-mono.sh. Move it into a shared locate_runtime_checkout function; build-mono.sh behaves as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Builds the meadow_assemblies package that Meadow apps are trimmed against and deployed with, ported from Legacy meadow-3.0: - System.Private.CoreLib from the pinned runtime (mono.corelib subset, linux-arm, FeaturePerfTracing=false); - the framework assemblies of the runtime's .NET SDK; - System.Security.Cryptography and six System.Net assemblies rebuilt from the runtime sources for Unix/Linux, so they use the P/Invokes Meadow.OS implements rather than the host OS's; - the CoreLib ILLink descriptor, which lets the Meadow CLI trim CoreLib; - without the assemblies that are never used on Meadow. Unlike the Legacy script it uses the MONO_RUNTIME_COMMIT pin check instead of cloning and re-branching the runtime, fails if any library build fails, and records the runtime commit and framework in build-info.txt. The output matches the 3.0.0 package's file list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the disabled bcl job, which ran a Legacy Mono BCL script that is not in this repository, with an assemblies job. It fetches the pinned runtime as the OS build does, runs build-managed-assemblies.sh and uploads meadow_assemblies (the DLLs, the CoreLib ILLink descriptor and build-info.txt) as an artifact. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Phase 5: managed assemblies and the 3.0 CLI deploy flow
2.999.0 is used by the Legacy 3.0 work. Use 2.999.50 for this (Rebase) OS so the two can be told apart, while staying at the 2.999 Meadow CLI 3.x threshold. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Set the OS version to 2.999.50
On NuttX 12, fcntl(F_SETFL) passes O_NONBLOCK to the socket interface as FIONBIO, and netdev_ioctl() stops at any error other than -ENOTTY. espcp forwarded FIONBIO to the ESP32, which rejected it with EINVAL, so fcntl(O_NONBLOCK) on every WiFi socket failed. Map FIONBIO to SIOCSESPNONBLOCK, which carries the ESP socket and the requested state ([command][ESP socket][nonblock]) so the ESP32 can set O_NONBLOCK on the lwip socket (Meadow-ESP32 d1890583). If the ESP32 rejects it (older firmware), return -ENOTTY so that the local flag is still set and fcntl() succeeds, with the ESP32 socket left blocking. Adapted from Legacy f79817ac67e, which hooked net_vfcntl() on NuttX 7. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
espcp refused SO_ERROR locally with -EPFNOSUPPORT, which is not -ENOPROTOOPT, so NuttX did not fall back to its own socket-level answer and .NET reported "Unknown socket error" when it completed a non-blocking connect. Send it to the ESP32, which returns and clears the pending socket error (Meadow-ESP32 b3102532), and decode the reply like SO_RCVBUF. From Legacy f35f586ce71. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The poll interrupt handler only set revents on the caller's pollfd. In NuttX 12 a driver reports events with poll_notify(), which sets revents and calls the pollfd's callback to wake the waiting poll(); without it the waiter slept until its timeout (the NuttX 7 code posted fds->sem). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The runtime's NuttX socket engine (WaitForSocketEventsInner in pal_networking.c) takes events only from a 0-timeout poll of every socket; its blocking poll only waits for a change and then samples again. A poll sent to the ESP32 cannot be answered inside a 0-timeout poll, so non-blocking connects never completed and async I/O depended on timing. Keep a readiness cache in espcp_usrsock_conn_s (ESPCP_SF_*) and answer polls from it in poll_setup without a round trip to the ESP32: - POLLOUT is answered locally unless writability is unknown (connect in progress, or the last send returned EAGAIN); while it is unknown, a POLLOUT reported by the ESP32 is cached (WR_READY). - POLLIN, POLLHUP and POLLERR come from the ESP32's poll interrupts; the readable bit is cleared when a receive returns EAGAIN. - connect, sendmsg, recvmsg and getsockopt(SO_ERROR) keep the write-side bits up to date. - A late interrupt only updates the cache if the connection still has the ESP socket the poll was set up on. Poll teardown now removes its request before checking whether the ESP32 is responding, so the request can no longer outlive the caller's pollfd. Ported from Legacy 65e257319d5, with WR_READY from f80001fc99c. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since NuttX 12.1 (abba05a) getifaddrs() and the other users of NET_SOCK_TYPE open a SOCK_CTRL socket just to issue interface ioctls. espcp passed the type to the ESP32 unchanged, and lwIP has no SOCK_CTRL, so socket() failed with EINVAL and getifaddrs() (and with it .NET's NetworkInterface.GetAllNetworkInterfaces()) failed. Ask the ESP32 for SOCK_DGRAM, which is what NET_SOCK_TYPE was before 12.1; the local socket keeps the SOCK_CTRL type. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
espcp marks its interface with the Meadow specific IFF_WIFI flag so that the interface can be identified as WiFi. It used (1 << 6), which NuttX 12 defines as IFF_POINTOPOINT. Use (1 << 14), which <net/if.h> leaves free. The runtime reads the flag as MEADOW_IFF_WIFI (src/mono/cmake/nuttx-compat.h) and the two values must match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ESP32 answers SIOCGIFFLAGS with IFF_UP when the station has an IP address and has no separate carrier state, so the interface never showed IFF_RUNNING. POSIX callers, .NET's NetworkInterface among them, need IFF_UP and IFF_RUNNING to report the interface as up, so set IFF_RUNNING together with IFF_UP. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Runtime #6 (merged as 300006899fb) fixes the Thumb-2 JIT encoding of ASR and ROR by register and makes NetworkInterface work on Meadow.OS: name servers from /meadow0/dns.conf, the interface type and MAC address from espcp's flags and SIOCGIFHWADDR, and the gateway from meadow_os_get_gateway_address(). The merge commit has the same tree as the runtime that was tested on the board. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
poll_setup() takes a reference on each file with file_get() and keeps it while poll() waits. poll_teardown() looked the descriptor up again by number, tore the poll down on whatever file it found and released two references from it. If another thread closes the descriptor while poll() waits, the number is free and is often reused straight away. Teardown then acted on the new file (dropping a reference it did not own) and never released the reference on the original file, so that file was never closed. If the number was not reused, file_get() failed and the reference was again never released. The .NET runtime's socket engine keeps every socket in a poll() on its own thread while other threads close them. Each HTTPS connection then left its socket open: sock_file_close() never ran, the usrsock close never reached the ESP32, and after about 14 connections socket() failed with ENFILE from the ESP32's lwIP socket table. poll() now keeps the struct file pointer that setup obtained for each entry (on the stack for up to 16 descriptors, on the heap above that) and teardown, including the cancellation cleanup, tears down and releases exactly that file. A descriptor closed during the poll is closed when the poll releases the last reference. Upstream NuttX has the same poll_teardown(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With mbedTLS 3.6.2 the TLS 1.3 client handshake fails with MBEDTLS_ERR_SSL_INTERNAL_ERROR while computing the Finished verify data (after CertificateVerify has passed), and the failure moves when diagnostics are added, so it is a race or a stack problem that is not yet found (issue #704). Legacy Meadow's mbedTLS 3.2.1 had TLS 1.3 compiled out, so TLS 1.2 is what the shim was validated with. Cap the shim's configuration at TLS 1.2. Over TLS 1.2 HTTPS to www.google.co.uk passes: 50/50 keep-alive requests, 100/100 full handshakes (about 780 ms each) and 20/20 full pages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CryptoNative_SslDoHandshake() kept the mbedTLS error in last_error but reported only SSL_ERROR_SSL, so a failed handshake showed nothing more than SslException in managed code. Print the error code and text, as CryptoNative_SslRead() already does, and for a certificate verify failure also the verify flags and their description. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
espcp_usrsock_shutdown() logged "not implemented" and returned EOPNOTSUPP, and .NET calls shutdown() (or disconnects) before close() on every socket to wake operations blocked on it, so each HTTPS close logged an error and nothing ended a poll waiting on the socket. Send the request to the ESP32 (WiFiFunction Shutdown, socket handle and how in the NuttX numbering, which the ESP32 translates to lwIP's) and return its result. An ESP32 without the handler answers with an error status, which is reported as EOPNOTSUPP as before. Needs the matching Meadow-ESP32 Shutdown handler. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The .NET runtime's NuttX socket engine leaves every registered socket in its blocking poll(), and a socket whose read and write readiness have both been reported is polled with no events at all. espcp sent each of these to the ESP32, where it held a poll worker and a wake socket (an lwIP socket) and cost a setup and a teardown message on every pass of the engine (at least every 100 ms), yet it could only end on a hang-up or an error. Answer a poll that asks for neither POLLIN nor POLLOUT locally: a hang-up or error already in the readiness cache is still reported, and one that arrives later is picked up by the next poll that asks for POLLIN or POLLOUT, which the engine makes as soon as a read or write on the socket would block. Teardown already treats a socket with no request as success. Tested on the board: the network tests pass (10/10), 15 concurrent sockets open (the ESP32's lwIP limit; 13-16 before), the burst test shows no leaked ESP32 sockets, and the TLS soak passes (50/50 keep-alive, 100/100 full handshakes, 20/20 full pages) slightly faster than the same build without this change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…p option Memory.md said DTCM was used for kernel data and stacks; nothing is placed there and CONFIG_STM32F7_DTCMEXCLUDE keeps it out of every heap. Record that, and the option (from Legacy f0b7762b4f7) of adding it to the kernel heap if espcp runs out of kernel heap, with what to check first: DMA into kmm_malloc() buffers, adding it to the kernel heap from board code, and keeping the last 256 bytes for the fault black box (#709). Not done now: no kernel-heap ENOMEM has been seen on Rebase. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Phase 6: networking and TLS for the .NET runtime
…mbers CLAUDE.md said the ARM toolchain is installed under toolchain/macos/ and added to PATH by build.sh; the directory is not in the repository, so the compiler (Arm GNU Toolchain 15.3.Rel1) has to be on PATH, as it is from Homebrew locally and in the CI image. Rebasing.md said the notes assume a rebase from 12.10 to 12.11; the tree is now 12.12 (f962d87), so say what they were written for. Also replace the non-ASCII box-drawing characters in its directory diagram with ASCII. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Documentation: toolchain note and Rebasing release numbers
|
Reference: the plan for this work, ManagedPlan.md (revision 3.11, 2026-10-16). It is kept outside the repository; local paths have been removed from this copy. Posted in two parts because of the comment size limit. ManagedPlan.md, part 1 of 2 (sections 1-3, phases 0-2)ManagedPlan.md - Adding Mono and the managed libraries to RebaseStatus: phases 0-3 merged into Revision history:
Sources used (local refs, not re-fetched on 2026-10-09):
Path convention: this file lives in the 1. SummaryLegacy Rebase today has the hooks but no runtime:
Where the work is (re-weighted in rev 3):
Recommended approach: a staged port. Boot first with as few changes as possible, then add Legacy 2. What Legacy meadow-3.0 contains2.1 Runtime (
|
| Item | Verified content |
|---|---|
mono_main.c (1853 lines, 177 non-ASCII bytes) |
meadow_mono_main: symtab_initialize(), then hcom_via_nx_copy_mono_runtime_to_ram(), zero _s_mono_bss.._e_mono_bss, setenv (below), TPA built from /meadow0/*.dll, dlopen of *.dll.so AOT images, monovm_initialize with TPA, APP_PATHS and PINVOKE_OVERRIDE, mono_set_optimizations(... GSHAREDVT), hcom_mono_ctrl_mono_appears_to_be_running() (clears the lockup bit, redirects stdio, sets the LED), mbedTLS warm-up thread (priority 50, 16 KB), monovm_execute_assembly of /meadow0/Meadow.dll. Also the meadow_pinvoke_override() resolver (lines 1084-1145) and a 247-entry System.Native table |
mono_nuttx_stubs.c (906 lines) |
__errno() returning one global int (lines 383-387); --wrap errno-sync wrappers for 30 calls (socket connect bind listen accept getsockopt setsockopt getsockname getpeername send sendto recv recvfrom close read write open fcntl ioctl lseek fstat stat ftruncate poll select pipe2 sem_wait sem_trywait sem_timedwait sem_post); a full replacement for mmap/munmap (SDRAM copy); libc gap stubs; up_invalidate_icache() implemented with cacheflush() (lines 731-734) |
mono_mbedtls.c (482) + pal_ssl_mbedtls.c (984) + root-ca-der.h |
An OpenSSL-shaped CryptoNative_* shim (84 entries) over mbedTLS. It uses only the public API (45 mbedtls_* functions, no MBEDTLS_PRIVATE) and never calls psa_crypto_init(). Root CAs: 147 arrays holding 153,298 DER bytes |
mappings-meadow.h |
DllImport("nuttx") table with 70 live entries. Lines 117-1085 are a commented-out table of about 970 entries (kernel internals). The file starts with a UTF-8 BOM |
mappings-sqlite.h |
133 live entries, names stored without the sqlite3_ prefix (the stated reason: "uflash is within bytes of full") |
mappings-mbedtls.h |
7 entries, but unreachable: the resolver has no "mbedtls" branch (see 3.2) |
symtab.c, symtab.inc (502 lines), generate-symtab.sh |
Registers the table with boardctl(BOARDIOC_OS_SYMTAB) (CONFIG_BOARDCTL_OS_SYMTAB), which serves the kernel-side loader of NuttX 7 |
Make.defs |
Links the runtime archives from $(TOPDIR)/../../runtime/src/mono/build-nuttx-$(MONO_CONFIG)/mono and a separate mbedTLS from $(TOPDIR)/../mbedtls (the wildernesslabs fork; the docs say 3.2.1). Adds the 32 --wrap flags to the whole user-image link |
user-space.ld |
.mono (SDRAM) holds the runtime archives plus the SQLite amalgamation object. libSystem.Native.a, mbedTLS and the glue stay in uflash. A global /DISCARD/ drops .ARM.exidx (R_ARM_PREL31 range workaround) |
stm32_allocateheap.c |
heap2_base = CONFIG_HEAP2_BASE + HCOM_NX_FS_MONO_RAW_PARTITION_SIZE (lines 417-418); the MPU still maps the full 32 MB |
hcom_mono_control.c delta |
10 insertions, 11 deletions: rename to meadow_mono_main, bypass the lockup bit, required file System.Private.CoreLib.dll |
meadow_liveness.c, SysTick IWDG kick, meadow_blackbox.h, fault hooks |
MEADOW_IWDG_BACKSTOP is defined in board.h; /dev/liveness (a write of 'D' disarms it) |
Settings made in mono_main.c:
MONO_GC_PARAMS=max-heap-size=8m,nursery-size=512k,soft-heap-limit=4m,major=marksweep. The same
string is also passed directly tomono_gc_params_set(), because the code comment saysgetenv()
may not seesetenv()in protected mode.DOTNET_ThreadPool_ForceMinWorkerThreads=4,ForceMax...=16,DOTNET_TieredCompilation=0.DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1,DOTNET_SYSTEM_RESOURCES_USESYSTEMRESOURCEKEYS=true,
DOTNET_SYSTEM_CONSOLE_SKIP_TERMINAL_INIT=1,TMPDIR=/meadow0/Temp.MONO_LOG_LEVEL=info,MONO_LOG_MASK=aot(diagnostic; revisit for production).
2.3a Legacy review after Phase 6 (2026-10-16)
Legacy meadow-3.0 and main commits compared with Rebase after Phase 6. Not in Rebase, filed
as "Investigation Needed":
- Mono glue: close() never calls the runtime's nxsock_close_notify(), so closed sockets stay in the socket engine's poll set #711 (closed, not needed): filed in error. The runtime's
SystemNative_Close()already calls
nxsock_close_notify()beforeclose()(Phase 3 runtime change, replacing Legacy cad9bb65513's
firmware__wrap_close); thepal_networking.ccomment saying it comes from__wrap_closeis stale. - Mono: disable unused runtime features to reclaim SDRAM (Legacy measured ~2.7 MB) #712: disable unused runtime features, ~2.7 MB SDRAM (Legacy e5a116dd5dc); may help GC stress under the JIT: OutOfMemoryException allocating large arrays (interpreter passes) #707.
- The DHCP setting is only sent to the ESP32 for a static IP, so it never raises NetworkGotIpEvent #713: the DHCP setting is only sent to the ESP32 for a static IP, so NetworkGotIpEvent is never
raised (Legacy c6936151e76); related to WiFi gateway is never known to the OS: the connected event carries gateway 0 #703. - espcp recvmsg(): an ESP32 error with errno 0 is returned as a clean end-of-file #714: espcp
recvmsg()returns an ESP32 error with errno 0 as end-of-file (idea of Legacy
fb54f810323, which fixedrecvfrom(), unused in Rebase). - OTA: meadow_cloud_decrypt_buf() never frees its mbedTLS contexts #715: OTA
meadow_cloud_decrypt_buf()never frees its mbedTLS contexts (Legacy 6a7171b2ebd).
Already in Rebase or superseded: QSPI (Recover from a stuck QSPI BUSY flag #702), the non-blocking bridge/SO_ERROR/readiness cache,
de4501854f7 and ee1faa97bf9 (superseded by the fs_poll fix 087fd11), the message_id
use-after-free, WiFi country code, the sleep() bug, BIO growth, mbedTLS warm-up. Recorded issues:
ESPCP: H12 - ~60 unchecked allocations inespcp_encoders.c#478/ESPCP: M5 -espcp_sock_addr_to_sockaddr()leaks on every call #481/ESPCP: M6 -espcp_usrsock_setsockopt()leaks on every unsupported option #482/ESPCP: M22 - Unchecked allocations inespcp_common.c/espcp_queue.c#497/ESPCP: M27 -espcp_get_message()robustness #501 (out-of-memory guards), espcp: port the remaining Legacy f80001fc99c hardening (bounded waits, SO_SNDTIMEO, poll timeouts, teardown retries) #708, Liveness: IWDG hardware backstop, DTCM fault black box and /dev/liveness (from Legacy) #709. NuttX TCP-stack fixes for Ethernet
and cell only, filed as "Investigation Needed": NuttX TCP: closing a socket during a blocking connect() leaves its callback on a freed connection (Ethernet/cell) #716 (close during a blocking connect leaves its
callback;tc_resultstarts at -EAGAIN; Legacy 6db7ad5b96b), NuttX: check that a failed protocol close does not leak the connection (Ethernet/cell) #717 (check that a failed protocol
close does not leak the conn; Legacy 2e1a41cae8f, probably not applicable as written in 12.12),
NuttX: SO_LINGER on native TCP sockets fails with ENOPROTOOPT, which breaks MQTTnet over Ethernet/cell #718 (SO_LINGER returns ENOPROTOOPT on native TCP, breaks MQTTnet; Legacy 2ccad46b68e).
2.3b Remaining plan items filed as issues (2026-10-16)
Everything left open in this plan outside phase 7 is now an issue (all assigned to NevynUK):
#724 Q8 remote debugging option (question); #725 MONO_LOG_LEVEL/MONO_LOG_MASK for production
(question); #726 Mono mini test suite; #720 atan(+-inf) workaround check; #727 GC under blocking
and network load, and commit the phase 6 test apps (R8, R18, R22); #728 P/Invoke startup self-test
(R26); #721 ConvertFileStatus zeros (runtime); #722 configure.cmake NuttX 12 features (runtime);
#723 stale __wrap_close comments (runtime); #729 offer the fs_poll fix upstream. Doc fixes
merged as #719 (fe3f929).
2.3 Legacy kernel/board patches: status against Rebase NuttX 12.12
| # | Legacy patch | Rebase 12.12 status (verified unless noted) | Action |
|---|---|---|---|
| 1 | tcp_connect.c/tcp.h: connect cancel, -EINPROGRESS |
Affects only NuttX's own TCP stack (Ethernet; PPP is not enabled in Rebase). WiFi sockets go through espcp usrsock. 12.12 code not compared | Only if Ethernet socket tests fail |
| 2 | net_close.c/inet_close.c: always release the slot |
The 12.12 socket layer is rewritten (sockets are files). Not compared | On demand |
| 3 | SIOCSESPNONBLOCK (non-blocking bridge) |
Absent from Rebase espcp, from Rebase esp32/, and from Meadow-ESP32 HEAD. It exists only on the unmerged branches meadow/espcp-nonblock-ioctl (798 commits off main) and meadow/espcp-worker-hardening (532) |
Decide (Q4). Without it the ESP side stays blocking, which was Legacy's fallback |
| 4 | ESP readiness cache in net.h |
Absent | Goes with #3 |
| 5 | Fake SO_LINGER, SO_RCVBUF/SO_SNDBUF |
NET_SOLINGER not set, NET_RECV_BUFSIZE=0; espcp option forwarding not checked |
Test with MQTTnet |
| 6 | FD_CLOEXEC kept in f_oflags |
Native in 12.12 (F_GETFD/F_SETFD/F_DUPFD_CLOEXEC, O_CLOEXEC) |
Drop |
| 7 | dlopen(NULL) sentinel |
Still needed. dlopen() -> dlinsert() -> strdup(NULL) (lib_dlopen.c:88; the DEBUGASSERT is compiled out by CONFIG_NDEBUG=y), and Mono calls dlopen(NULL) |
Fix in the runtime (HOST_NUTTX in mono_dl_open_self) or the glue, not in NuttX libc |
| 8 | ramlog overwrites the oldest data | Native in 12.12 (ramlog_copybuf writes modulo the buffer size) |
Drop |
| 9 | cdcacm initial SerialState (macOS open) | 12.12 sends DSR/DCD SerialState from its SET_CONTROL_LINE_STATE path when input flow control is off. Not Mono-specific | Drop unless macOS open problems reappear |
| 10 | DTCM added to the kernel heap (128 -> 256 KB) | Not present (CONFIG_STM32F7_DTCMEXCLUDE=y; kernel heap 128 KB) |
Port if espcp ENOMEM appears under TLS/MQTT churn |
| 11 | QSPI bounded BUSY waits, ABORT recovery, retries | Ported to main by PR #702 (with the AN4838 strongly ordered MPU change from Legacy 21c33c52169) | Done |
| 12-13 | IWDG kick in SysTick, liveness gate, DTCM black box | Not present (CONFIG_STM32F7_IWDG not set) |
Optional, phase 6. Meadow.Core pets /dev/liveness only if the device exists |
| 14 | NET_NACTIVESOCKETS=32 |
The symbol does not exist in 12.12. The equivalent caps are NET_USRSOCK_PREALLOC_CONNS=6 with NET_USRSOCK_ALLOC_CONNS=0 (6 WiFi sockets in total) and NET_TCP_PREALLOC_CONNS/ALLOC_CONNS=8/8 (Ethernet) |
Raise the usrsock limit (R24) |
| 15 | espcp fixes (UAF, leaks, bounded waits, poll) | Rebase espcp was reworked separately (for example the file_mq_* conversion 0728a9b and the review fixes). No fix-by-fix comparison done |
Compare before the networking phase |
3. Rebase current state and gaps (verified in rev 3)
| Area | Rebase today | Gap |
|---|---|---|
| HCOM Mono control | apps/system/hcom/mono/* is built (Makefile:66-68). The start call is #if(0) (hcom_startup_manager.c:547-549). task_create at hcom_mono_control.c:454-461 uses mono_main_proxy when CONFIG_HCOM_MONO_REMOTE_DEBUGGING=y (the current .config), and that proxy's Mono call is //FIXME (line 946); (main_t)NULL is the other branch. A monitor thread waitpid()s and resets the board when Mono exits (lines 364-390) |
Provide meadow_mono_main; re-enable; update the required-files list to System.Private.CoreLib.dll |
| Kconfig | Board Kconfig:16-22 defines HCOM_MONO_STDERR_STDOUT (default y; .config not set) and HCOM_MONO_REMOTE_DEBUGGING (default y; .config y). CONFIG_HCOM_MONO_STDXXX is referenced but undefined (RebaseNotes item 7). CONFIG_EXAMPLES_MONO survives only as a comment in the defconfig |
Define the Mono app symbol; settle the STDXXX rename |
| Linker | .mono_signature sits at 0xC0000000 (user-space.ld:121-142) and is stamped with version, build date, hash and MEADOW_GIT_REF (32 bytes) by scripts/version_methods.sh:97-109. The .mono* sections (lines 144-219) are commented out and list Mono 6.9 archive names |
Rewrite with the .NET archive names |
| SDRAM | stm32_allocateheap.c registers all 32 MB from 0xC0000000 as user heap. The MPU region (stm32_mpu_uheap, stm32_mpuinit.c:103-119) is `TEX_SO |
C |
| Image packaging | build.sh:528-530 uses dd ... bs=1024 skip=3014400 count=3072. That is exact: (0xC0000000 - 0x08040000) / 1024 = 3014400. The current Meadow.OS.Runtime.bin holds a valid signature (aabbccdd, version, "Fix-Issue261"). Side effect: nuttx_user.bin is 3,086,745,643 bytes |
Works. Optionally switch to objcopy -j to avoid the 2.9 GB file; add a size check |
| Runtime write and copy | The CLI writes /meadow0/Meadow.OS.Runtime.bin, which must be exactly 3 MB (hcom_nx_fs_flash.c:681). hcom flashes it to the raw QSPI partition at offset 0 and deletes the file (:1111-1122). HCOM_NX_UPD_COPY_RUNTIME_TO_RAM copies 3 MB to CONFIG_HEAP2_BASE (meadow-upd.c:370-377). The config manager reads the signature from block 0 (hcom_nx_config_manager.c:3228-3249) |
Call it from meadow_mono_main (as Legacy does) |
| Partitions | 3 MB raw runtime partition at QSPI offset 0 (hcom_shared_common.h:90) |
Keep the image under 3 MB (runtime archives plus anything moved there, see 3.1) |
| dlopen | LIBC_DLFCN, ELF, LIBC_ELF on. Symbols resolve through dlsymtab() with an explicit export table (apps/system/hcom/tests/sotest_tests.c:110); CONFIG_LIBC_ELF_HAVE_SYMTAB is not needed. nuttx/libs/libc/elf/elf_globals.S (4316 lines, committed; generated by nuttx/tools/build-globals.sh). ET_DYN relocation fixes are merged (PR #232) |
Needed for AOT only; use dlsymtab(), not boardctl |
| FDs and IPC | EVENT_FD, TIMER_FD, SIGNAL_FD, FS_NOTIFY off; epoll with 8 descriptors; FIFO and pipe 1024 bytes; FS_ANONMAP=y, FS_RAMMAP off; PSEUDOFS_SOFTLINKS off (no readlink()) |
The PAL uses poll and pipe (as in Legacy); decide mmap and readlink (phase 3) |
| Sockets | espcp usrsock sockif with poll and SOCKCAP_NONBLOCKING; NET_USRSOCK_NPOLLWAITERS=1; 6 usrsock connections maximum; DNS through the NuttX client (/meadow0/dns.conf); the sockif getaddrinfo path is not wired; SO_* forwarding not checked |
Raise the cap; test SocketAsyncEngine |
| Scheduler and threads | PRIORITY_INHERITANCE, CANCELLATION_POINTS, SCHED_HAVE_PARENT off; SCHED_WAITPID=y; PTHREAD_MUTEX_TYPES and ROBUST on; TLS_NELEM=32; TLS_NCLEANUP=8; RR_INTERVAL=200; 1 ms tick; PTHREAD_STACK_DEFAULT=65536; FDCLONE_STDIO=y (the Mono task inherits only stdio) |
Section 4, phase 4 |
| Signals | MAX_SIGNO=63, SIGRTMIN=32; SIG_DEFAULT off; SIG_PREALLOC_ACTIONS=4 |
Mono's 20/21/22 are SIGTSTP/SIGTTIN/SIGTTOU here (phase 1) |
| libm | CONFIG_LIBM_TOOLCHAIN=y: newlib's libm is whole-archived into the user link; there is no nuttx/include/math.h |
The runtime's NuttX-libm atan workaround may be moot |
| TLS | mbedTLS 3.6.2 in user space (about 1050 mbedtls_* symbols in nuttx_user.elf, none in the kernel). MEADOW_CLOUD=y selects CRYPTO_MBEDTLS. TLS 1.2 and 1.3, PSA crypto, ALPN and session tickets on; MBEDTLS_THREADING_C off |
Port the shim to this copy; no second mbedTLS |
| SQLite | CONFIG_LIB_SQLITE=y (3.45.1) in user space. All 133 live Legacy SQLite P/Invoke entries resolve |
None for the bindings |
| Watchdog | IWDG and WWDG off; software meadow_watchdog slots only; Mono liveness is waitpid plus the BBR lockup bit (BK31R bits 0x800, 0x1000, 0x2000; hcom_bbreg_defn.h:106-110) |
Optional IWDG and liveness (phase 6) |
/dev/upd |
Has open, close, read and ioctl (meadow-upd.c:159-165), so an O_RDONLY open works (Legacy's EACCES note does not apply). The ioctl numbers match Meadow.Core (3.2) |
Keep the numbers; ConsolidateUpdDrivers.md must preserve them |
| GPIO from user mode | mpu_user_peripheral(STM32_GPIOA_BASE, ...) (stm32_boot.c:257); stm32_configgpio/gpiowrite/unconfiggpio are syscalls |
None |
| Filesystem | LittleFS v2.5.1 (Legacy: v1.7, disk format 1.1) | Expected re-deploy of all assemblies when moving a device from Legacy to Rebase |
Note: CodeReviewTracking.md lists items "Blocked (runtime)" (#325, #326, #327, #393; B43-B45,
C123). They become testable once Mono runs. B44 (#326, remote VS debug) stays untestable, because
the runtime is built without the debugger agent (R29).
3.1 Flash and RAM budget (new in rev 3)
Measured from the ELFs of the 2026-10-09 build:
| Region | Used | Size | Free |
|---|---|---|---|
| uflash (user code, 0x08040000) | 791 KiB (.text 809,448 bytes) |
1024 KiB | about 233 KiB |
| kflash (kernel code, 0x08140000) | 320 KiB | 768 KiB | about 448 KiB |
| SDRAM image region (0xC0000000) | 43 bytes (signature only) | 3072 KiB | - |
- Symbol sizes in the user image: SQLite about 181 KiB, mbedTLS about 182 KiB.
- With the Legacy layout, uflash would also receive
libSystem.Native.a(size unknown until
built), the glue (mono_main.cwith its tables, the stubs and the TLS shim) and the 153 KB root-CA
table. Legacy, which had already moved SQLite into SDRAM, reported uflash "within bytes of full". - Options, which can be combined:
- (a) Move SQLite into the SDRAM image, as Legacy did.
- (b) Move
libSystem.Native.aand the root-CA table into the SDRAM image. - (c) Load the root CAs from a file in
/meadow0. This also lets the CAs be updated without a
reflash. - (d) Move the uflash/kflash boundary inside the fixed 1792 KiB OS slot, since kflash has about
448 KiB spare. This means changingmemory.ldand checkingbuild.shpacking and the
bootloader before relying on it.
- SDRAM heap after the offset: about 29 MB. Legacy found that a bigger GC heap hurts, because SGen
and mbedTLS share it, and that about 6 MB per TLS connection is native memory outside SGen. - The kernel heap is 128 KB (ksram) and the espcp bridge allocates from it (Legacy patch Added boardctl.h to Rebase, it may not be required #10).
3.2 Managed ABI: Meadow.Core meadow-3.0 against Rebase (new in rev 3)
- Target framework: all 16 csproj files target
net10.0; they run on the .NET 11-preview Mono
runtime, which was the Legacy combination. "nuttx"imports: Meadow.Core declares 50 entry points (LibraryImport(LIBRARY_NAME),
Interop.cs:7). 46 resolve in Rebase's user libraries. The 4 missing are
meadow_cell_change_state,meadow_cell_is_connected,meadow_get_cell_at_cmds_outputand
meadow_get_cell_error; cellular support is disabled in Rebase. Provide "not supported" stubs or
port the cell work."mbedtls"import:mono_mbedtls_set_server_cert_authmode(Interop.mbedtls.cs:9), called
from the public API inF7PlatformOS.cs:179. Legacy's resolver has no"mbedtls"branch, so this
appears to fail on Legacy 3.0 as well. Add the branch; the 7-entrymappings-mbedtls.htable
already exists.- Legacy table entries absent in Rebase: besides the 4 cell functions,
meadow_rotary_encoder_read_count. Rebase replaced it with the struct-based
meadow_rotary_encoder_read_data()and added a trailingdelayMsfield to the config struct.
Meadow.Coremeadow-3.0does not call the rotary encoder, so drop or update the table entry. /dev/upd: Meadow.CoreUpdIoctlFn(Interop.upd.cs:142-171) matches RebaseMUPD_*exactly
for codes 1-72, 81 and 82. Codes 74 and 75 (PowerWDSet/PowerWDPet) exist only in Meadow.Core
and are not implemented in Legacy or Rebase. Code 73 (PWR_SLEEP2) exists only in Rebase.- Liveness:
/dev/livenessis optional on the managed side; it is only written when
File.Exists("/dev/liveness")(MeadowCloudConnectionService.cs:941-944). - Meadow.CLI
meadow-3.0: writesMeadow.OS.Runtime.binthrough HCOM and trims apps with
ILLink.Descriptors.xmlfromMeadowAssembliesPath(BuildManager.cs:418-421). The local
checkouts of Meadow.Core and Meadow.CLI are ondevelop, so use theirmeadow-3.0branches. - Coordinated
meadow-3.0branches exist in Meadow.Core, Meadow.CLI, Meadow.Contracts,
Meadow.Foundation, Meadow.Logging, Meadow.Units and Meadow.Modbus. MQTTnet, Meadow.Samples and
Meadow.TestSuite have none.
4. Plan
Principle: every phase ends with something observable on hardware. Per project rules, do not commit
until the user has tested on hardware ("a clean build is not enough"), and fix -Werror root
causes rather than suppressing warnings.
4.0 Working environment and branch model (set up 2026-10-14)
-
Worktree:
OS/Rebase.worktrees/MergeManagedCode, a git worktree of the Rebase repository
(OS/Rebase). All work on this plan happens there; the main checkout (OS/Rebase) stays free
for other work.git worktree listfrom either checkout shows both. -
Integration branch:
MergeManagedCode, created frommainand fast-forwarded on
2026-10-14 to 94de840 ("Merge pull request Synchronising build with Legacy #692 from WildernessLabs/UpdateBuildToMatchLegacy").
Pushed tooriginand trackingorigin/MergeManagedCode. -
Merge policy (user, 2026-10-14): Claude may push branches and create PRs, but must not merge
anything intomain. MergingMergeManagedCodeintomainis the user's decision. -
Phase branches: one per phase, named
Phase0,Phase1, ...Phase7. Each is created from
MergeManagedCodeand merged back intoMergeManagedCode, never intomain.
MergeManagedCodegoes tomainonce the work is complete.main --- MergeManagedCode ---+-------------+---------- ... --> main \ / \ / Phase0 ---+ Phase1 --+- Start a phase:
git switch MergeManagedCode && git switch -c PhaseNin the worktree, after
merging the previous phase. - Open each phase as a GitHub PR from
PhaseNwith baseMergeManagedCode. - Keep
MergeManagedCodecurrent withmainby mergingmaininto it (it is shared and pushed,
so do not rebase it). - Per project rules, commit only after the user confirms a hardware test, and confirm before
pushing or opening a PR.
- Start a phase:
-
Build state:
./build.sh --configuresucceeds in the worktree (2026-10-14) and leaves the tree
clean:build.shrestores the files it stamps (user-space.ld,hcom_nuttx_shared.h,
build_info.template). The worktree has its ownnuttx/.configand build outputs, separate from
OS/Rebase. For a first full build use./build.sh --clean --force. -
Sibling-path caution: from the worktree,
../runtimeresolves to
OS/Rebase.worktrees/runtime, which does not exist. The runtime clone is atOS/runtime(still
onmain; phase 0 switches it tomeadow-3.0). -
Runtime location:
MEADOW_RUNTIME_DIR(decided 2026-10-14). The Mono build scripts written
in phase 1 read the runtime location from the environment variableMEADOW_RUNTIME_DIR.- If it is not set, the default is
../runtime, resolved against the Rebase checkout's root (the
directory holdingbuild.sh), not against the current directory. InOS/Rebasethis gives
OS/runtime, so the main checkout needs no setup. - In the worktree the default resolves to
OS/Rebase.worktrees/runtime, which does not exist,
so set the variable there:export MEADOW_RUNTIME_DIR=<OS>/runtime(the runtime checkout beside the Rebase checkout). - The scripts fail with a clear message if the directory is missing, rather than cloning a copy
as Legacy'sbuild-mono.shdoes. They also fail if its HEAD is not the pinned commit (phase 0). - The scripts print the resolved path and commit at the start of every build.
- The runtime's own
build-nuttx.shdefaultsNUTTX_INCLUDE_DIRto../Meadow/nuttx/include;
always pass that explicitly as well. ../Legacy(reference only) and../Meadow-ESP32have the same worktree caveat.
- If it is not set, the default is
-
CI and artifacts:
.github/workflows/build-system.yml("Build OS and RT") runs
./build.sh --force --cleanin theDockerFiles/Buildimage. Since PR Synchronising build with Legacy #692 it uploads the
artifactsMeadow.OS(Meadow.OS.bin,Meadow.OS.Update.bin,Meadow.OS.Runtime.bin,
build-info.json) andMeadow.OS Debug Symbols(both ELFs and maps); run ENTER_DFU_MODE request writes a DFU magic value that nothing reads, so the board only restarts #818 kept them for 90
days. Abcljob exists but is disabled (if: false) and still calls Legacy's
build-mono-bcl.shand the Legacymono/externalsubmodules. The submodule step is disabled too.- The workflow only triggers on pushes to
mainand PRs targetingmain. PRs into
MergeManagedCodeget no build or artifacts untilMergeManagedCodeis added to both branch
lists. This is a phase 0 change. - Prebuilt runtime archives and the BCL should come from this CI as artifacts (user, 2026-10-14);
replace thebcljob in phase 5.
- The workflow only triggers on pushes to
-
This document lives in
OS/, outside both checkouts, and is not under git.
Phase 0 - Decisions, inputs and repository plumbing
Branch Phase0, merged into MergeManagedCode without a hardware test, since it changes nothing on
the board (user, 2026-10-14). Contents:
- The pinned-commit file
MONO_RUNTIME_COMMITat the repo root (commit f980b4c169b and tree
a108891e0a87). - Add
MergeManagedCodeto the workflow'spushandpull_requestbranch lists, so phase PRs
build and upload artifacts. - Outside git: switch
OS/runtimetomeadow-3.0and check that the runtime's SDK bootstrap works
on this Mac.
Status (2026-10-14): done and merged. PR #693 (Phase0 -> MergeManagedCode) passed CI run #819 and was merged as 77d4051.
- Commit 198b989 adds
MONO_RUNTIME_COMMIT(keysrepository,branch,commit,tree;
#comments) and addsMergeManagedCodeto the workflow'spushandpull_requestbranches. - PR Phase 0: pin the Mono runtime and build MergeManagedCode in CI #693 triggered CI run hcom_host_receive_takesem() and hcom_host_send_transmit_takesem() cannot retry after EINTR (wrong errno test) #819, so the new trigger works.
OS/runtimeis switched tomeadow-3.0and tracksorigin/meadow-3.0. HEAD is f980b4c169b and
the tree is a108891e0a87; a fresh fetch showed no newer commits.- SDK bootstrap:
./dotnet.sh --versioninOS/runtimeinstalled 11.0.100-preview.3.26161.119
intoOS/runtime/.dotnet(1 min 45 s, macOS arm64). The runtime checkout stays clean
(.dotnetis ignored). - A shell check of
MONO_RUNTIME_COMMITagainstMEADOW_RUNTIME_DIR(default../runtime)
matches. Phase 1's build script implements the same check.
Decisions and inputs:
- Runtime: check out
origin/meadow-3.0(f980b4c169b, tree a108891e0a87) in../runtime.
mainis plain upstream. Record the pin in Rebase (for example aMONO_RUNTIME_COMMITfile) and
make the build fail on a mismatch instead of falling back tomain. Do not use Legacy's
branch-name matching. - SDK: the runtime's own bootstrap installs
11.0.100-preview.3.26161.119into
runtime/.dotnet. The machine has SDKs 8.0.419 and 10.0.201, which are not used for this. Legacy
found that installing the SDK by hand left targeting packs missing. - Toolchain (decided 2026-10-09): GCC 15. Build the runtime with the same compiler as the rest
of Rebase:- Locally: Homebrew
arm-none-eabi-gcc15.3.1. - CI: Arm GNU Toolchain 15.3.rel1 in the build image (
DockerFiles/Build/Dockerfile:66). - Why this is the only consistent choice: Rebase cannot move to an older compiler anyway. The
committed dlopen export tablenuttx/libs/libc/elf/elf_globals.Sis generated by
nuttx/tools/build-globals.shagainst GCC 15.3's newlib, and an older toolchain fails the
user-space link (Dockerfile:52-65). One compiler for NuttX, newlib, libgcc and the runtime also
avoids mixing support libraries. - Legacy CI used GCC 10.3-2021.10 and CMake 3.30.5 on Linux x86_64 (
setup-ci-host.sh). That
was a requirement of Legacy's NuttX 7 tree (its CLAUDE.md); no runtime-specific reason is
recorded, and the runtime fork already contains a GCC 14+ accommodation (phase 1). - GCC 15's C23 default does not affect the runtime, which pins C11 and C++17
(eng/native/configurecompiler.cmake:6-9,src/mono/CMakeLists.txt:10-11). - Host tools: CMake 4.4.4 and ninja. The
toolchain/directory that CLAUDE.md describes does not
exist. - Consequences: the
int32_tpointer warnings in phase 1 (kept as warnings by decision), and R27.
- Locally: Homebrew
- Where the runtime lives:
MEADOW_RUNTIME_DIR(default../runtime, section 4.0) plus the
pinned-commit file. Prebuilt runtime archives come from CI artifacts (section 4.0). - Managed side: use the
meadow-3.0branches listed in 3.2. - ESP32 firmware: non-blocking bridge deferred; decided later, before phase 6 (Q4).
- First-milestone scope (decided): JIT only. No AOT, cloud, MQTT or cell; SQLite optional.
- Sign-off hardware (decided): CCM only (F7CoreComputeV2). F7FeatherV2 is similar enough that
it is not part of the test matrix. - Renode emulator: nice to have, deferred.
Phase 1 - Build the runtime against Rebase's NuttX 12.12 headers
Decisions (user, 2026-10-14):
- Runtime changes go on a new branch in WildernessLabs/runtime,
MergeManagedCode, created
frommeadow-3.0(f980b4c169b).meadow-3.0itself is never changed: Legacy'sbuild-mono.sh
checks it out by name, so changing it would change what Legacy 3.0 builds. - Runtime changes are reviewed as PRs in the runtime repository, from a per-phase branch
(Phase1, ...) into the runtime'sMergeManagedCode, mirroring the Meadow.OS branch model.
After a runtime PR merges, the Meadow.OSPhaseNPR movesMONO_RUNTIME_COMMITto the new
runtime commit and tree. The user's push/PR permission covers the runtime repository too (public,
admin access); never change itsmainormeadow-3.0. - Standalone script: phase 1 adds
build-mono.shto Meadow.OS, which builds only the runtime
archives.build.shand CI call it from phase 3, when the archives are first linked. CI then
needs a shallow fetch of the pinned runtime commit (the full history is 1.3 GB). - No hardware test for phase 1; nothing reaches the board. It merges on a clean build and CI.
- Signals: move Mono's suspend/restart/abort signals from 20/21/22 to
SIGRTMIN-based
numbers. - Build type: Release by default; Debug as an option (
--debug, as Legacy).
Status (2026-10-14): done and merged, no hardware test (as agreed). runtime#2 merged as c668e4a3aea (tree unchanged, 5cf48371256). MONO_RUNTIME_COMMIT was moved to it (branch=MergeManagedCode, commit 69100f2; CI run #822 passed). #694 merged as 50fa130.
-
Runtime PR: Phase 1: build the NuttX runtime against NuttX 12 headers with GCC 15 runtime#2,
Phase1-> runtimeMergeManagedCode(which was
created frommeadow-3.0f980b4c169b and pushed). Five commits, ending at a76e66e60c1
(tree 5cf48371256):- 05460ac745a: definition/declaration mismatches exposed by
int32_t=long
(sgen_client_binary_protocol_sweep_begin/end,
ves_icall_RuntimeType_GetFunctionPointerTypeModifiers,mono_regression_test_step,
SystemNative_Select). Also skips theh_errnostatic asserts on NuttX (NuttX swaps
NO_DATA/TRY_AGAIN; the values are never passed to managed code) and removes the
sys/random.hoverride. - c7dd57f3807: retires the NuttX 7 workarounds: errno override,
-DO_CLOEXEC=0, most of
nuttx-compat.h,pal_config.hflags (14 now enabled),HAVE_MKDTEMP. - 535eae386ce:
dlopen(NULL)sentinel inmono-dl-posix.c. - ae2e81b67f0: suspend/restart/abort signals become
SIGRTMIN + 1/2/3. - a76e66e60c1:
MONO_BUILD_DIRinbuild-nuttx.sh;nxsock_close_notifyprototype.
- 05460ac745a: definition/declaration mismatches exposed by
-
Meadow.OS PR: Phase 1: build the Mono runtime against NuttX 12 with build-mono.sh #694,
Phase1->MergeManagedCode(84ace53): addsbuild-mono.sh,
ignoresmonoruntime/, and pointsMONO_RUNTIME_COMMITat the runtimePhase1commit.
Merge order: runtime#2 first; then moveMONO_RUNTIME_COMMIT(commit, tree,
branch=MergeManagedCode) to the runtime merge commit in Phase 1: build the Mono runtime against NuttX 12 with build-mono.sh #694; then merge Phase 1: build the Mono runtime against NuttX 12 with build-mono.sh #694. -
Results:
- The baseline (unmodified runtime against NuttX 12 with GCC 15) failed with 24 errors from one
mismatch. - After the fixes, clean Release and Debug builds pass, 358/358 targets, in about 15 s each on
this Mac. All nine archives the OS link needs are produced (libmonosgen-2.0.ais 23 MB
Release, 24.6 MB Debug;libSystem.Native.a309 KB). - The Meadow.OS worktree build (
./build.sh --clean --force) still passes. - The
build-mono.shguards were exercised: no runtime at the default path, an unpinned
commit and a dirty checkout each fail with a message, and the stamp forces a rebuild after
a source change.
- The baseline (unmodified runtime against NuttX 12 with GCC 15) failed with 24 errors from one
-
Warning review (decision follow-up): all 86
-Wincompatible-pointer-typeswarnings are
32-bitint/longaliasing; none is a real pointer mismatch (Debug reports one site twice).
The other warnings remain as warnings, not errors:- 368
-Wformat, the sameint32_t=longeffect in format strings. - Upstream-code warnings that GCC 15 is stricter about:
-Wreturn-local-addr
(metadata.c:2273),-Warray-bounds(interp/transform.h:435,mini-arm.c:3059),
-Waddress(driver.c),-Wmaybe-uninitialized(w32handle.c:635,
pal_networking.c:3957),-Wmisleading-indentation(mini-exceptions.c:2565).
- 368
-
Corrections to earlier revisions:
- The
dlopen(NULL)call that matters ismono_dl_open_self()->mono_dl_open(NULL)
(mono-dl.c:91) ->mono_dl_open_file()->dlopen(NULL, flags)(mono-dl-posix.c:99).
Themono-dl.c:86call cited before is Android-only. HAVE_DLFCN_Hstays 0 inconfigure.cmake; that only affects eglib's gmodule.
- The
-
Follow-ups (later phases):
- The NuttX branch of
ConvertFileStatus(pal_io.c, about line 220) still hard-codes uid,
gid and the nanosecond times to 0, although NuttX 12'sstruct statnow has them. Revisit
when the file API is tested (phase 5). - Still-disabled
configure.cmakeentries that NuttX 12 now supports:HAVE_GETRLIMIT,
HAVE_PTHREAD_SETNAME_NP/GETNAME_NP,HAVE_STRERROR_R,HAVE_OPENLOG. They are
behaviour changes (for example named threads), so they are left for a deliberate change. - Test the
atan(+-inf)workaround on hardware (phase 4).
- The NuttX branch of
-
Port
build-mono.sh: locate the runtime throughMEADOW_RUNTIME_DIR(default../runtime
relative to the Rebase root; section 4.0), drop the clone and branch sync, check the pinned
commit, and passNUTTX_INCLUDE_DIRexplicitly. -
Re-evaluate the NuttX 7.29 compensations against Rebase
nuttx/include(all checked):nuttx-include-overrides/sys/random.h: delete. It assumes a 2-argumentgetrandom(); NuttX
12.12 declares the POSIXssize_t getrandom(void *, size_t, unsigned int), so the wrapper's
2-argument call will not compile.nuttx-include-overrides/errno.h: NuttX 12 already defineserrnoas*__errno(), a
per-thread lvalue, so the override is redundant. The real hazard is in the glue (phase 3).nuttx-compat.h:UINT32_C/INT32_C,realpath,mkdtemp,readlink(declaration only),
lstat,posix_memalignand_SC_PAGE_SIZEall exist in NuttX 12, so most of the file can go.
Re-checkmalloc_getpagesizeandCONFIG_C99_BOOL8.-DO_CLOEXEC=0: remove it. NuttX 12 supportsO_CLOEXEC(1<<10).- Signals: 20/21/22 collide with
SIGTSTP/SIGTTIN/SIGTTOUin NuttX 12. They would still
work, becauseSIG_DEFAULTis off, but real-time signals 32-63 are now available. Prefer
SIGRTMIN-based numbers. - Cooperative suspend: keep it. NuttX 12 queues signals while
tcb->sigdeliveris set instead
of refusing them. Whether a restart signal can reach a thread parked insigsuspend()inside a
handler is untested. pal_config.h: regenerate it by review against NuttX 12 (struct statfields,getifaddrs,
sendmsg/recvmsg,pipe2,epoll,sem_clockwait,pthread_attr_getstack, all of which are
present in user space now).- libm: with
LIBM_TOOLCHAIN,math.hcomes from newlib. Test whether theatan(+-inf)
workaround is still needed. int32_tislongon NuttX 12, and GCC 15 makes the mismatch an error.- NuttX 12 takes
_int32_tfrom the compiler's__INT32_TYPE__
(arch/arm/include/types.h:61-70), which islong intfor arm-none-eabi GCC 15.3. NuttX 7
usedsigned int, and the runtime's toolchain file still assumes that. - Mono code that mixes
int *withint32_t *orgint32 *therefore hits
incompatible-pointer-types, which GCC 14 and later treat as an error. The fork currently
downgrades it with-Wno-error=incompatible-pointer-typesunderHOST_NUTTX. - Options considered: (a) fix the mismatches in the runtime; (b) keep the downgrade; (c) build
the runtime withint32_tasint. - Decision (2026-10-09): (b), keep the flag in the runtime. Scope of the exception:
- It covers only the existing
-Wno-error=incompatible-pointer-types, underHOST_NUTTX,
in the runtime'ssrc/mono/CMakeLists.txt. - The no-suppression rule still applies to everything in Rebase, including the new Mono glue
underapps/. - Any other GCC 14+ error in the runtime is fixed, not suppressed.
- It covers only the existing
- Follow-up: the flag also hides genuine pointer-type bugs, such as mismatched struct pointers.
Once the first runtime build is clean, review itsincompatible-pointer-typeswarnings and
confirm they are all 32-bitint/longaliasing, which is harmless on ARM. Keep that list
in the build notes so new entries stand out.
- NuttX 12 takes
- Other GCC 14+ hard errors: implicit function declarations and int-conversion. Any NuttX 12
header gap left after removing the compat header shows up as an error, not a warning, which
helps thepal_config.hreview. dlopen(NULL): return a sentinel underHOST_NUTTXinmono_dl_open_self(), instead of
patching NuttX libc.
-
Deliverable:
libmonosgen-2.0.aand the companion archives built for Rebase. No linking yet.
Phase 2 - Memory map, linker and image packaging
Status (2026-10-14): done and merged. PR #695 (Phase2 -> MergeManagedCode) passed CI run #824 and was merged as 4625c4f.
Commits: 78ddd75 (stamped copy of user-space.ld) and 453e60f (SDRAM layout). The
hardware test on CCM passed (below).
-
Heap, by configuration, not by patching NuttX:
- The defconfig sets
CONFIG_HEAP2_BASE=0xc0300000andCONFIG_HEAP2_SIZE=30408704
(29 MB, to the end of SDRAM). stm32_allocateheap.c(upstream NuttX) is unchanged. Legacy patched it instead.
- The defconfig sets
-
Runtime copy destination: the copy ioctl in
meadow-upd.cnow copies to the new
HCOM_NX_MONO_RUNTIME_SDRAM_ADDR(0xC0000000, defined inhcom_shared_common.h) instead of
CONFIG_HEAP2_BASE. -
MPU:
stm32_boot.cmaps the 3 MB runtime area explicitly, with the same attributes as the
user heap. The heap's own region already covers it through 4 MB subregion rounding, but only by
accident of the sizes. -
user-space.ld: the.mono*output sections come first inSECTIONS:/DISCARD/of the runtime archives' unwind tables..mono_signature..mono(text),.mono_data(rodata and data) and.mono_bss(NOLOAD).
All use the nine .NET archive names except
libSystem.Native.a, which stays in uflash as in
Legacy; its placement is decided in phase 3. First-match placement removes Legacy's
EXCLUDE_FILElists, and the old Mono 6.9 comment block is gone. -
build.sh:check_mono_sdram_layoutcross-checks the four descriptions of the layout
(header address and size,memory.ldsdram, the heap config,SDRAM_SIZE) and stops the
build on any mismatch; tested with two deliberate mismatches. TheMeadow.OS.Runtime.bin
ddnow uses offsets derived frommemory.ld(still 3014400 KB in, 3072 KB long). -
Build fix found on the way:
generate_build_infoused togit checkout HEAD
user-space.ldand stamp the version into it in place. That silently discarded uncommitted
linker-script edits; it happened during this phase, and Legacy had the same trap. The version is
now stamped into an untracked copy,user-space.stamped.ld, which the user link uses
(kernel/Makefile).restore_versioned_filesno longer touchesuser-space.ld. -
Heap report: hcom logs
User heap: <total> bytes in total, <free> bytes free(LOG_NOTICE)
once at startup, to verify this phase and to track memory use later (R7). -
Docs:
Documentation/Memory.mdSDRAM section rewritten, the QSPI raw runtime partition
added, and the uflash/kflash table corrected. -
Hardware test (2026-10-14, run by Claude on the attached CCM, F7CoreComputeV2):
- Flash:
flash.shover the ST-Link. The board was in DFU mode, and SWD flashing works
regardless.verify_imagematched all 2,097,152 bytes, including the CRC at0x081FFFFC. - Boot: clean, three times. Logs:
p2boot.stm.log/p2boot.esp.log, captured with
meadow_capture.py.User heap: 30507004 bytes in total, 30434992 bytes free: the 29 MB SDRAM region
(30,408,704) plus about 98 KB of internal SRAM, 3,145,728 bytes less than the old layout.- No fault, assert or stack dump.
- Third boot explained: the runtime already in QSPI (
Mono version: 2.5.7.4, so the
signature read works) does not match OS 1.99.0.19732. hcom disabled Mono and reset
(Mono:Enabled->Mono:Disabled), which is the intended safety behaviour (user, 2026-10-14).
There were no further resets for the rest of the capture. - CLI:
meadow device inforeports OS 1.99.0.19732 and the coprocessor version, so the
ESP32 link works. - Files: a 200 KB random file written, read back (SHA-1 identical) and deleted.
- Not tested: WiFi (no network configured on this board), and writing this build's
Meadow.OS.Runtime.bin, which would replace the runtime in QSPI.
- Flash:
-
Original test list for the user (CCM): flash
nuttx/Meadow.OS.binfrom the worktree; check
that it boots and that the log shows the user heap at about 3 MB less than before (the SDRAM
part is now 30,408,704 bytes); check normal operation (CLI, file write, WiFi). Optionally write
this build'sMeadow.OS.Runtime.binand check that the runtime version is reported, which
exercises the partition write and the signature read. -
Heap: in
stm32_allocateheap.c, add the heap2 region from
CONFIG_HEAP2_BASE + HCOM_NX_FS_MONO_RAW_PARTITION_SIZE, as Legacy does. Keep the MPU mapping of
the full 32 MB (the attributes already match Legacy). Add a boot-time assert that the heap starts
at or above_e_mono_bss. -
Linker: rewrite the
.mono,.mono_data,.mono_bssand.mono_commentsections, the
EXCLUDE_FILElists and the PHDRS for the .NET archive names. Decide the flash placement from 3.1
at the same time. -
Packaging: keep the
ddextraction, which is correct. Add a check that the SDRAM sections fit
in 3 MB. Optionally useobjcopy -jto avoid the 2.9 GB intermediate file. -
Signature: already stamped by
scripts/version_methods.sh; no work. (Rev 1 said this was
missing; it is not.) -
Deliverable: the link succeeds and
Meadow.OS.Runtime.binholds the runtime. The OS and runtime
images must come from the same build (R3).
🤖 Generated with Claude Code
ManagedPlan.md, part 2 of 2 (phases 3-7, risks, questions, notes, change and verification logs)Phase 3 - Glue code and Kconfig
Phase 3 status (2026-10-14): implemented and boot-tested on CCM. Runtime#3 merged (520e78f6c99,
Phase 4 - Config alignment and first boot (JIT, console only)
Phase 4 status (2026-10-14): Hello World and GC stress pass on CCM (JIT and interpreter); merged (Meadow.OS #697 -> 5b650ff; runtime #4 ff60710f833, #5 fafd81e8032). Blinky passes (2026-10-15).
Phase 5 - Managed libraries and deployment
Phase 5 status (2026-10-15): deliverable met on Project Lab v3; merged (#699 -> 6bcad8a).
Phase 6 - Networking, TLS, liveness (the long tail)Each item is an independent change with its own hardware test.
Final PRs (2026-10-16): Meadow.OS #731 (MergeManagedCode -> main) and Meadow-ESP32 #610 (MergeManagedCode -> develop), both mergeable and awaiting review. The runtime stays as is, pinned at its latest MergeManagedCode commit 300006899fb. Phase 6 status: complete (2026-10-16). Merged into MergeManagedCode: Meadow.OS #710 (a9f2faa) and Meadow-ESP32 #609 (d1cebd137b0). Remaining work is in issues: #703, #704, #706, #707 (managed port), #708 and #709 (deferred enhancements), and the ESPCP code-review issues (#478, #481, #482, #497, #501 and others), which stay out of the merge work. Earlier status notes follow. Phase 6 status (2026-10-15): in progress. Meadow.OS branch
Phase 7 - AOT and dlopen (optional, not needed for parity)Filed as enhancement #730 (2026-10-16); not started.
5. Risks and mitigations
6. Open questions for the user
7. Notes and caveats
8. Change log
9. Verification log (rev 3)
🤖 Generated with Claude Code |
Summary
Brings the Mono/.NET managed runtime onto Rebase (NuttX 12.12): the work of ManagedPlan.md phases 0-6, each of which was reviewed, hardware-tested on a Meadow F7 CCM (ProjectLab v3) and merged into
MergeManagedCode.build-mono.shapps/system/mono), Kconfig and SDRAM placementbuild-managed-assemblies.sh) and the 3.0 CLI deploy flowpoll()fix, TLS 1.2 cap)Runtime
The runtime is built from WildernessLabs/runtime, pinned in
MONO_RUNTIME_COMMITat MergeManagedCode 300006899fb (tree e5888e3c96f). That is the latest commit on that branch and includes runtime #4, #5 and #6:The runtime repository itself is not changed by this PR.
Companion firmware
The ESP32 changes are in WildernessLabs/Meadow-ESP32
MergeManagedCode(PR intodevelop):SO_ERROR;The NuttX 12 value mapping in that firmware is needed by this OS.
Tested
On a Meadow F7 CCM, OS/runtime 2.999.50.17, ESP32 2.5.7.568 with the Meadow-ESP32 changes:
meadow app run;Known issues and follow-ups (filed)
🤖 Generated with Claude Code