From 0b5713955ccaa7f994df0f8a805b3f98d303dfc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Montiel=20Cardona?= Date: Thu, 25 Jun 2026 16:46:49 -0600 Subject: [PATCH 01/10] Extract ClrInspector/ClrLayout into NanoFramework.Clr.Core; add profiling to the harness ClrInspector and ClrLayout move to the chip-agnostic NanoFramework.Clr.Core and now read through an IClrMemory; this kit supplies the RP2040 adapter (Rp2040ClrMemory over the emulator bus). The CLR walker no longer references RP2040 at all, so STM32/ESP32 kits will reuse it instead of duplicating it. The harness gains CaptureHeap() (dotMemory-style heap snapshots) and ProfileCalls() (dotTrace-style per-method call counts + instructions/cycles). --- .../ClrInspector.cs | 235 ------------------ .../ClrLayout.cs | 97 -------- .../NanoClrHarness.cs | 81 +++++- .../RP2040Sharp.NanoFramework.TestKit.csproj | 3 + .../Rp2040ClrMemory.cs | 22 ++ 5 files changed, 105 insertions(+), 333 deletions(-) delete mode 100644 src/RP2040Sharp.NanoFramework.TestKit/ClrInspector.cs delete mode 100644 src/RP2040Sharp.NanoFramework.TestKit/ClrLayout.cs create mode 100644 src/RP2040Sharp.NanoFramework.TestKit/Rp2040ClrMemory.cs diff --git a/src/RP2040Sharp.NanoFramework.TestKit/ClrInspector.cs b/src/RP2040Sharp.NanoFramework.TestKit/ClrInspector.cs deleted file mode 100644 index c0a7ad1..0000000 --- a/src/RP2040Sharp.NanoFramework.TestKit/ClrInspector.cs +++ /dev/null @@ -1,235 +0,0 @@ -using System.Text; -using RP2040.Peripherals; - -namespace RP2040Sharp.NanoFramework.TestKit; - -/// -/// Reads managed state out of a running nanoCLR by walking its in-RAM data structures with a -/// . Currently resolves a managed static field by name — assembly -/// (g_CLR_RT_TypeSystem.m_assemblies) → type/field (the .pe metadata tables) → the -/// cross-reference slot → the CLR_RT_HeapBlock that holds the value. -/// -public sealed class ClrInspector -{ - private readonly RP2040Machine _m; - private readonly ClrLayout _l; - private readonly uint _typeSystem; - - public ClrInspector(RP2040Machine machine, uint typeSystemAddress, ClrLayout? layout = null) - { - _m = machine; - _typeSystem = typeSystemAddress; - _l = layout ?? ClrLayout.Default; - } - - private uint Rd(uint a) => _m.Bus.ReadWord(a); - private ushort Rh(uint a) => _m.Bus.ReadHalfWord(a); - private byte Rb(uint a) => _m.Bus.ReadByte(a); - - private string CStr(uint a) - { - var sb = new StringBuilder(); - for (byte c = Rb(a); c != 0; c = Rb(++a)) - { - sb.Append((char)c); - } - - return sb.ToString(); - } - - /// The CLR_RT_Assembly pointer for the loaded assembly named (0 if none). - public uint FindAssembly(string name) - { - uint max = Rd(_typeSystem + _l.TS_AssembliesMax); - for (uint i = 0; i < max && i < 64; i++) - { - uint asm = Rd(_typeSystem + _l.TS_Assemblies + i * 4); - if (asm == 0) - { - continue; - } - - uint namePtr = Rd(asm + _l.ASM_Name); - if (namePtr != 0 && CStr(namePtr) == name) - { - return asm; - } - } - - return 0; - } - - /// - /// Resolves the static-field slot (the cross-reference offset into m_pStaticFields) for the - /// static field named in assembly . Searches every - /// type's static fields; pass to disambiguate a repeated field name. - /// - public bool TryResolveStaticSlot(uint asm, string fieldName, out int slot, string? typeName = null) - { - slot = -1; - uint header = Rd(asm + _l.ASM_Header); - uint sot = header + _l.SOT; - uint typeDefTable = header + Rd(sot + (uint)_l.TBL_TypeDef * 4); - uint fieldDefTable = header + Rd(sot + (uint)_l.TBL_FieldDef * 4); - uint stringHeap = header + Rd(sot + (uint)_l.TBL_Strings * 4); - - uint xref = Rd(asm + _l.ASM_XrefFieldDef); - if (xref == 0 || Rd(asm + _l.ASM_StaticFields) == 0) - { - return false; // the CLR has not finished allocating this assembly's statics yet - } - - // TypeDef record count = byte span to the next table / record size (tables are contiguous). - uint typeDefBytes = Rd(sot + (uint)(_l.TBL_TypeDef + 1) * 4) - Rd(sot + (uint)_l.TBL_TypeDef * 4); - int typeCount = (int)(typeDefBytes / _l.TD_Size); - - for (int t = 0; t < typeCount; t++) - { - uint td = typeDefTable + (uint)t * _l.TD_Size; - if (typeName != null && CStr(stringHeap + Rh(td + _l.TD_Name)) != typeName) - { - continue; - } - - int first = Rh(td + _l.TD_SFieldsFirst); - int num = Rb(td + _l.TD_SFieldsNum); - for (int fi = first; fi < first + num; fi++) - { - uint fd = fieldDefTable + (uint)fi * _l.FD_Size; - if (CStr(stringHeap + Rh(fd + _l.FD_Name)) == fieldName) - { - slot = Rh(xref + (uint)fi * 2); // CLR_RT_FieldDef_CrossReference[fi].m_offset - return true; - } - } - } - - return false; - } - - /// A static field's stored cell: its nanoCLR data type and the raw 32 bits of value. - public readonly record struct HeapValue(byte DataType, uint Raw) - { - public int AsInt32 => (int)Raw; - public uint AsUInt32 => Raw; - public bool AsBoolean => Raw != 0; - } - - /// Reads the static field's storage cell (its data type byte + the value's low 32 bits). - public HeapValue ReadStatic(uint asm, int slot) - { - uint heapBlock = Rd(asm + _l.ASM_StaticFields) + (uint)slot * _l.HB_Size; - byte dataType = Rb(heapBlock); // CLR_RT_HeapBlock.m_id.dataType @ 0 - return new HeapValue(dataType, Rd(heapBlock + _l.HB_Data)); - } - - /// Reads a managed static int field as a 32-bit value. - public int ReadStaticInt32(uint asm, int slot) => ReadStatic(asm, slot).AsInt32; - - /// Reads a managed static long field as a 64-bit value (the cell's 8 data bytes). - public long ReadStaticInt64(uint asm, int slot) - { - uint hb = Rd(asm + _l.ASM_StaticFields) + (uint)slot * _l.HB_Size; - ulong lo = Rd(hb + _l.HB_Data); - ulong hi = Rd(hb + _l.HB_Data + 4); - return (long)(lo | (hi << 32)); - } - - // ---- instance fields (by name) ------------------------------------ - - /// - /// Resolves the instance-field slot for of - /// (the offset, in heap blocks, of the field within an object of that type). - /// - public bool TryResolveInstanceSlot(uint asm, string typeName, string fieldName, out int slot) - { - slot = -1; - uint header = Rd(asm + _l.ASM_Header); - uint sot = header + _l.SOT; - uint typeDefTable = header + Rd(sot + (uint)_l.TBL_TypeDef * 4); - uint fieldDefTable = header + Rd(sot + (uint)_l.TBL_FieldDef * 4); - uint stringHeap = header + Rd(sot + (uint)_l.TBL_Strings * 4); - uint xref = Rd(asm + _l.ASM_XrefFieldDef); - if (xref == 0) - { - return false; - } - - uint typeDefBytes = Rd(sot + (uint)(_l.TBL_TypeDef + 1) * 4) - Rd(sot + (uint)_l.TBL_TypeDef * 4); - int typeCount = (int)(typeDefBytes / _l.TD_Size); - - for (int t = 0; t < typeCount; t++) - { - uint td = typeDefTable + (uint)t * _l.TD_Size; - if (CStr(stringHeap + Rh(td + _l.TD_Name)) != typeName) - { - continue; - } - - int first = Rh(td + _l.TD_IFieldsFirst); - int num = Rb(td + _l.TD_IFieldsNum); - for (int fi = first; fi < first + num; fi++) - { - uint fd = fieldDefTable + (uint)fi * _l.FD_Size; - if (CStr(stringHeap + Rh(fd + _l.FD_Name)) == fieldName) - { - slot = Rh(xref + (uint)fi * 2); - return true; - } - } - } - - return false; - } - - /// - /// Reads instance field of the object at . The slot is - /// the field's cross-reference m_offset, which already counts past the object header (the nanoCLR does - /// res = Dereference(); res += m_offset), so it is not additionally offset by the header. - /// - public HeapValue ReadInstance(uint obj, int slot) - { - uint hb = obj + (uint)slot * _l.HB_Size; - return new HeapValue(Rb(hb), Rd(hb + _l.HB_Data)); - } - - // ---- methods (stack frames) --------------------------------------- - - private uint StringHeapOf(uint asm) - { - uint header = Rd(asm + _l.ASM_Header); - return header + Rd(header + _l.SOT + (uint)_l.TBL_Strings * 4); - } - - /// - /// The managed method a CLR_RT_StackFrame is running, as "Assembly!Method" - /// (m_call.m_assm.m_szName + m_call.m_target's name). "" if is not one. - /// - public string MethodAt(uint stackFrame) - { - uint asm = Rd(stackFrame + _l.SF_CallAssm); - if (asm == 0) - { - return string.Empty; - } - - uint namePtr = Rd(asm + _l.ASM_Name); - uint md = Rd(stackFrame + _l.SF_CallTarget); - if (namePtr == 0 || md == 0) - { - return string.Empty; - } - - string asmName = CStr(namePtr); - string method = CStr(StringHeapOf(asm) + Rh(md + _l.MD_Name)); - return asmName + "!" + method; - } - - // ---- instance heap objects (arrays) ------------------------------- - - /// Element count of a managed array object (its CLR_RT_HeapBlock_Array header). - public uint ReadArrayLength(uint arrayObject) => Rd(arrayObject + _l.ARR_NumElems); - - /// Reads element of a managed int[] object. - public int ReadArrayInt32(uint arrayObject, int index) => (int)Rd(arrayObject + _l.ARR_DataOff + (uint)index * 4); -} diff --git a/src/RP2040Sharp.NanoFramework.TestKit/ClrLayout.cs b/src/RP2040Sharp.NanoFramework.TestKit/ClrLayout.cs deleted file mode 100644 index 75c6564..0000000 --- a/src/RP2040Sharp.NanoFramework.TestKit/ClrLayout.cs +++ /dev/null @@ -1,97 +0,0 @@ -namespace RP2040Sharp.NanoFramework.TestKit; - -/// -/// In-RAM struct offsets of a specific nanoCLR build, used to read managed state (e.g. a static -/// field) out of the emulator's memory. The values come from the firmware's DWARF -/// (arm-none-eabi-gdb -batch nanoCLR.elf -ex 'ptype /o struct …') and are therefore tied to -/// the build — keep them next to the firmware. matches the vendored RP_PICO build. -/// -public sealed record ClrLayout -{ - // CLR_RT_TypeSystem (g_CLR_RT_TypeSystem) - public uint TS_Assemblies { get; init; } // m_assemblies[64] - public uint TS_AssembliesMax { get; init; } // m_assembliesMax - - // CLR_RT_Assembly - public uint ASM_Header { get; init; } // m_header (-> CLR_RECORD_ASSEMBLY in flash) - public uint ASM_Name { get; init; } // m_szName (const char*) - public uint ASM_StaticFields { get; init; } // m_pStaticFields (CLR_RT_HeapBlock[]) - public uint ASM_XrefFieldDef { get; init; } // m_pCrossReference_FieldDef (CLR_RT_FieldDef_CrossReference[]) - - // CLR_RT_HeapBlock - public uint HB_Size { get; init; } // sizeof(CLR_RT_HeapBlock) - public uint HB_Data { get; init; } // m_data (numeric value lives here) - - // CLR_RECORD_ASSEMBLY (.pe header) - public uint SOT { get; init; } // startOfTables[16] (CLR_OFFSET_LONG, 4 bytes each) - - // metadata table indices - public int TBL_TypeDef { get; init; } - public int TBL_FieldDef { get; init; } - public int TBL_Strings { get; init; } - - // CLR_RECORD_TYPEDEF - public uint TD_Size { get; init; } - public uint TD_Name { get; init; } // CLR_STRING (offset into the string heap) - public uint TD_SFieldsFirst { get; init; } // first static FieldDef index - public uint TD_SFieldsNum { get; init; } // static field count (1 byte) - public uint TD_IFieldsFirst { get; init; } // first instance FieldDef index - public uint TD_IFieldsNum { get; init; } // instance field count (1 byte) - - // CLR_RECORD_FIELDDEF - public uint FD_Size { get; init; } - public uint FD_Name { get; init; } // CLR_STRING - - // CLR_RECORD_METHODDEF - public int TBL_MethodDef { get; init; } - public uint MD_Size { get; init; } - public uint MD_Name { get; init; } // CLR_STRING - - // CLR_RT_StackFrame.m_call (CLR_RT_MethodDef_Instance) - public uint SF_CallAssm { get; init; } // m_call.m_assm (CLR_RT_Assembly*) - public uint SF_CallTarget { get; init; } // m_call.m_target (CLR_RECORD_METHODDEF*) - - // Instance object / array layout (object data is a CLR_RT_HeapBlock[]; arrays add a header) - public uint OBJ_FieldsOff { get; init; } // object pointer -> first instance field - public uint ARR_NumElems { get; init; } // CLR_RT_HeapBlock_Array.m_numOfElements - public uint ARR_SizeElem { get; init; } // .m_sizeOfElement - public uint ARR_DataOff { get; init; } // first element - - // CLR_RT_Assembly.m_pTablesSize[16] — per-table record counts. - public uint ASM_TablesSize { get; init; } - - /// Offsets for the vendored RP_PICO (RP2040) nanoCLR build. - public static ClrLayout Default { get; } = new() - { - TS_Assemblies = 0, - TS_AssembliesMax = 256, - ASM_Header = 20, - ASM_Name = 24, - ASM_TablesSize = 32, - ASM_StaticFields = 96, - ASM_XrefFieldDef = 128, - HB_Size = 12, - HB_Data = 4, - SOT = 40, - TBL_TypeDef = 4, - TBL_FieldDef = 5, - TBL_Strings = 11, - TD_Size = 24, - TD_Name = 0, - TD_SFieldsFirst = 16, - TD_SFieldsNum = 20, - TD_IFieldsFirst = 18, - TD_IFieldsNum = 21, - FD_Size = 8, - FD_Name = 0, - TBL_MethodDef = 6, - MD_Size = 16, - MD_Name = 0, - SF_CallAssm = 44, - SF_CallTarget = 48, - OBJ_FieldsOff = 12, - ARR_NumElems = 12, - ARR_SizeElem = 17, - ARR_DataOff = 20, - }; -} diff --git a/src/RP2040Sharp.NanoFramework.TestKit/NanoClrHarness.cs b/src/RP2040Sharp.NanoFramework.TestKit/NanoClrHarness.cs index dc5bbc2..5fba453 100644 --- a/src/RP2040Sharp.NanoFramework.TestKit/NanoClrHarness.cs +++ b/src/RP2040Sharp.NanoFramework.TestKit/NanoClrHarness.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; +using NanoFramework.Clr; using RP2040.Core.Cpu; using RP2040.TestKit.Boards; @@ -175,7 +177,84 @@ public void OnInstruction(uint pc, ushort opcode, long cycles) private ClrInspector? _inspector; /// Reads managed CLR state out of the emulator (needs g_CLR_RT_TypeSystem in the manifest). - public ClrInspector Clr => _inspector ??= new ClrInspector(Pico.Rp2040, Firmware.ResolveSymbol("g_CLR_RT_TypeSystem")); + public ClrInspector Clr => _inspector ??= + new ClrInspector(new Rp2040ClrMemory(Pico.Rp2040), Firmware.ResolveSymbol("g_CLR_RT_TypeSystem")); + + // ---- profiling: heap (dotMemory-style) + call counts (dotTrace-style) ---- + + /// + /// Snapshots the managed heap — used/free bytes and a per-kind object breakdown — for memory + /// benchmarking. Capture two and call after.Since(before) to see what an operation allocated. + /// Needs g_CLR_RT_ExecutionEngine in the firmware manifest; use the overload otherwise. + /// + public ClrInspector.HeapSnapshot CaptureHeap() => Clr.CaptureHeap(Firmware.ResolveSymbol("g_CLR_RT_ExecutionEngine")); + + /// Snapshots the managed heap using an explicit g_CLR_RT_ExecutionEngine address. + public ClrInspector.HeapSnapshot CaptureHeap(uint executionEngineAddress) => Clr.CaptureHeap(executionEngineAddress); + + /// A call-count profile: total instructions/cycles over a run and how often each managed method was entered. + public sealed record CallProfile(long Instructions, long Cycles, IReadOnlyDictionary CallCounts); + + /// + /// Runs the CLR (profiled) until holds, tallying every managed method entry + /// (each Execute_IL) by "Assembly!Method" and counting instructions/cycles — a dotTrace-style + /// "what ran, and how often". Needs Execute_IL in the manifest. + /// + public CallProfile ProfileCalls(Func until, long maxInstructions = 200_000_000) + { + uint executeIl = Firmware.ResolveSymbol("Execute_IL"); + var prof = new CallProfiler(Pico.Cpu, Clr, executeIl); + + long remaining = maxInstructions; + while (remaining > 0 && !until()) + { + int chunk = (int)Math.Min(remaining, 1_000_000); + Pico.Rp2040.RunProfiled(chunk, prof); + remaining -= chunk; + } + + return new CallProfile(prof.Instructions, prof.CycleSpan, prof.Calls); + } + + // Tallies Execute_IL entries by method (the CLR_RT_StackFrame& is the call arg → R0 or R1) and + // counts executed instructions / cycle span over the profiled window. + private sealed class CallProfiler(CortexM0Plus cpu, ClrInspector clr, uint executeIl) : IProfilingObserver + { + private readonly Dictionary _calls = new(); + private long _firstCycle = -1; + + public long Instructions { get; private set; } + public long CycleSpan { get; private set; } + public IReadOnlyDictionary Calls => _calls; + + public void OnInstruction(uint pc, ushort opcode, long cycles) + { + Instructions++; + if (_firstCycle < 0) + { + _firstCycle = cycles; + } + + CycleSpan = cycles - _firstCycle; + + if (pc != executeIl) + { + return; + } + + string method = clr.MethodAt(cpu.Registers.R1); + if (method.Length == 0) + { + method = clr.MethodAt(cpu.Registers.R0); + } + + if (method.Length != 0) + { + _calls.TryGetValue(method, out int n); + _calls[method] = n + 1; + } + } + } /// Reads a managed static field's cell (data type + raw value) by name (assembly must be loaded). public ClrInspector.HeapValue ReadStatic(string assembly, string field) diff --git a/src/RP2040Sharp.NanoFramework.TestKit/RP2040Sharp.NanoFramework.TestKit.csproj b/src/RP2040Sharp.NanoFramework.TestKit/RP2040Sharp.NanoFramework.TestKit.csproj index 09e13d8..f7555f2 100644 --- a/src/RP2040Sharp.NanoFramework.TestKit/RP2040Sharp.NanoFramework.TestKit.csproj +++ b/src/RP2040Sharp.NanoFramework.TestKit/RP2040Sharp.NanoFramework.TestKit.csproj @@ -26,6 +26,9 @@ + + diff --git a/src/RP2040Sharp.NanoFramework.TestKit/Rp2040ClrMemory.cs b/src/RP2040Sharp.NanoFramework.TestKit/Rp2040ClrMemory.cs new file mode 100644 index 0000000..70d4731 --- /dev/null +++ b/src/RP2040Sharp.NanoFramework.TestKit/Rp2040ClrMemory.cs @@ -0,0 +1,22 @@ +using NanoFramework.Clr; +using RP2040.Peripherals; + +namespace RP2040Sharp.NanoFramework.TestKit; + +/// +/// Adapts an RP2040Sharp machine's bus to the chip-agnostic so the shared +/// can read this emulator's RAM. This is the one RP2040-specific seam the +/// CLR walker needs; the rest of the introspection lives in NanoFramework.Clr.Core. +/// +public sealed class Rp2040ClrMemory : IClrMemory +{ + private readonly RP2040Machine _machine; + + public Rp2040ClrMemory(RP2040Machine machine) => _machine = machine; + + public uint ReadWord(uint address) => _machine.Bus.ReadWord(address); + + public ushort ReadHalfWord(uint address) => _machine.Bus.ReadHalfWord(address); + + public byte ReadByte(uint address) => _machine.Bus.ReadByte(address); +} From 7a2ee86de51968da6562ce9f085f12d0eb59d888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Montiel=20Cardona?= Date: Thu, 25 Jun 2026 17:01:52 -0600 Subject: [PATCH 02/10] Make NanoClrHarness a facade over ClrSession via an IClrHost adapter The run/profile logic moves to NanoFramework.Clr.Core's ClrSession; this kit now supplies Rp2040ClrHost (IClrHost over PicoSimulation + NanoFirmware, bridging the profiling observer and exposing R0/R1) and NanoApp.FromDeployment (wrap a prebuilt deployment.bin). NanoClrHarness keeps its public API but delegates everything. Reference path follows the NanoFramework.Clr umbrella rename. --- .../NanoApp.cs | 8 + .../NanoClrHarness.cs | 357 +++--------------- .../RP2040Sharp.NanoFramework.TestKit.csproj | 2 +- .../Rp2040ClrHost.cs | 62 +++ 4 files changed, 124 insertions(+), 305 deletions(-) create mode 100644 src/RP2040Sharp.NanoFramework.TestKit/Rp2040ClrHost.cs diff --git a/src/RP2040Sharp.NanoFramework.TestKit/NanoApp.cs b/src/RP2040Sharp.NanoFramework.TestKit/NanoApp.cs index a83f6ab..f17115c 100644 --- a/src/RP2040Sharp.NanoFramework.TestKit/NanoApp.cs +++ b/src/RP2040Sharp.NanoFramework.TestKit/NanoApp.cs @@ -83,6 +83,14 @@ public static NanoApp FromPeFiles(string peDirectory, IReadOnlyList asse return new NanoApp(ms.ToArray(), assemblyOrder, checksums); } + /// + /// Wraps an already-assembled deployment image (e.g. a prebuilt deployment.bin) as-is. No + /// checksum guard is applied unless is supplied — the caller + /// vouches that the deployment matches the firmware. + /// + public static NanoApp FromDeployment(byte[] deployment, IReadOnlyDictionary? nativeChecksums = null) + => new(deployment, Array.Empty(), nativeChecksums ?? new Dictionary()); + // nanoFramework PE header: marker "NFMRK1" at 0, nativeMethodsChecksum (UINT32 LE) at offset 20. private const int NativeChecksumOffset = 20; private static readonly byte[] Marker = "NFMRK1"u8.ToArray(); diff --git a/src/RP2040Sharp.NanoFramework.TestKit/NanoClrHarness.cs b/src/RP2040Sharp.NanoFramework.TestKit/NanoClrHarness.cs index 5fba453..b0b58ef 100644 --- a/src/RP2040Sharp.NanoFramework.TestKit/NanoClrHarness.cs +++ b/src/RP2040Sharp.NanoFramework.TestKit/NanoClrHarness.cs @@ -1,21 +1,20 @@ -using System.Collections.Generic; using NanoFramework.Clr; -using RP2040.Core.Cpu; using RP2040.TestKit.Boards; namespace RP2040Sharp.NanoFramework.TestKit; /// -/// Boots a deployed nanoFramework app on the RP2040Sharp emulator and drives it to points of -/// interest inside the running CLR — a generic condition () or a native CLR -/// function located by symbol () — so a test can assert on what the -/// firmware is actually doing, not just on wall-clock slices. +/// Boots a deployed nanoFramework app on the RP2040Sharp emulator and drives it to points of interest +/// inside the running CLR — a generic condition (), a native function by symbol +/// (), a managed method, or a static-field predicate — so a test can +/// assert on what the firmware is actually doing, not just on wall-clock slices. /// /// -/// Both run methods drive the emulator's hook-aware execution path (the one that services the -/// bootrom/flash native hooks the firmware needs to boot). watches -/// every executed instruction's PC through the profiling observer, so the crossing into the native -/// function is detected exactly — never sampled past. +/// This is the RP2040 façade over the chip-agnostic : it owns the emulator and +/// the boot, and adapts them through . All the run/snapshot/profile logic +/// lives in NanoFramework.Clr.Core and is shared with the other chips' kits and the profiler. The run +/// methods drive the emulator's hook-aware execution path (the one that services the bootrom/flash +/// hooks the firmware needs to boot). /// public sealed class NanoClrHarness : IDisposable { @@ -25,17 +24,15 @@ public sealed class NanoClrHarness : IDisposable public NanoFirmware Firmware { get; } public NanoApp App { get; } - /// The symbol last reached by (null if none). - public string? LastReachedSymbol { get; private set; } - - /// The cycle count when was reached (-1 if none). - public long LastReachedCycle { get; private set; } = -1; + /// The chip-agnostic session driving the CLR — exposed for advanced/cross-chip use. + public ClrSession Session { get; } private NanoClrHarness(PicoSimulation pico, NanoFirmware firmware, NanoApp app) { Pico = pico; Firmware = firmware; App = app; + Session = new ClrSession(new Rp2040ClrHost(pico, firmware)); } /// @@ -49,324 +46,76 @@ public static NanoClrHarness Boot(NanoFirmware firmware, NanoApp app, bool withU return new NanoClrHarness(pico, firmware, app); } - /// The current CPU program counter. - public uint Pc => Pico.Cpu.Registers.PC; - - public bool IsLockedUp => Pico.Cpu.IsLockedUp; + /// The symbol last reached by (null if none). + public string? LastReachedSymbol => Session.LastReachedSymbol; - public long InstructionCount => Pico.InstructionCount; + /// The cycle count when was reached (-1 if none). + public long LastReachedCycle => Session.LastReachedCycle; - /// - /// Runs the CLR in hook-aware slices until holds (checked at slice - /// boundaries — apt for monotonic, observable state such as a probe's captured-word count). - /// Returns false if is reached first. - /// - public bool RunUntil(Func ready, long maxInstructions = 200_000_000, int slice = 100_000) - { - long ran = 0; - while (ran < maxInstructions && !ready()) - { - Pico.RunInstructions(slice); - ran += slice; - } + /// The current CPU program counter. + public uint Pc => Session.Pc; - return ready(); - } + public bool IsLockedUp => Session.IsLockedUp; - /// - /// Runs the CLR until the CPU executes the native function (resolved - /// from the firmware manifest) — i.e. until managed code crosses into that InternalCall. Every - /// executed instruction's PC is watched, so the crossing is caught exactly. On success - /// / record where and when. - /// - public bool RunUntilNativeCall(string symbol, long maxInstructions = 200_000_000) - { - uint target = Firmware.ResolveSymbol(symbol); - var watch = new PcWatch(target); + public long InstructionCount => Session.InstructionCount; - long remaining = maxInstructions; - while (remaining > 0 && !watch.Hit) - { - int chunk = (int)Math.Min(remaining, 1_000_000); - Pico.Rp2040.RunProfiled(chunk, watch); - remaining -= chunk; - } + /// Reads managed CLR state out of the emulator (needs g_CLR_RT_TypeSystem in the manifest). + public ClrInspector Clr => Session.Clr; - if (watch.Hit) - { - LastReachedSymbol = symbol; - LastReachedCycle = watch.HitCycle; - } + /// + public bool RunUntil(Func ready, long maxInstructions = 200_000_000, int slice = 100_000) + => Session.RunUntil(ready, maxInstructions, slice); - return watch.Hit; - } + /// + public bool RunUntilNativeCall(string symbol, long maxInstructions = 200_000_000) + => Session.RunUntilNativeCall(symbol, maxInstructions); - /// - /// Runs the CLR until a managed method executes — i.e. until CLR_RT_Thread::Execute_IL is - /// entered for a stack frame whose method is ("Assembly!Method", e.g. - /// a generated AppSymbols.Methods.Main). Needs Execute_IL in the firmware manifest. - /// + /// public bool RunUntilManagedMethod(string methodFqn, long maxInstructions = 200_000_000) - { - uint executeIl = Firmware.ResolveSymbol("Execute_IL"); - var watch = new MethodWatch(Pico.Cpu, Clr, executeIl, methodFqn); + => Session.RunUntilManagedMethod(methodFqn, maxInstructions); - long remaining = maxInstructions; - while (remaining > 0 && !watch.Hit) - { - int chunk = (int)Math.Min(remaining, 1_000_000); - Pico.Rp2040.RunProfiled(chunk, watch); - remaining -= chunk; - } + /// + public ClrCallProfile ProfileCalls(Func until, long maxInstructions = 200_000_000) + => Session.ProfileCalls(until, maxInstructions); - if (watch.Hit) - { - LastReachedSymbol = methodFqn; - LastReachedCycle = watch.HitCycle; - } + /// + public ClrInspector.HeapSnapshot CaptureHeap() => Session.CaptureHeap(); - return watch.Hit; - } + /// + public ClrInspector.HeapSnapshot CaptureHeap(uint executionEngineAddress) => Session.CaptureHeap(executionEngineAddress); - /// Element count of a managed static int[] field (a heap-allocated array object). - public uint StaticArrayLength(string assembly, string field) => Clr.ReadArrayLength(ResolveStaticArray(assembly, field)); + /// + public uint StaticArrayLength(string assembly, string field) => Session.StaticArrayLength(assembly, field); - /// Reads element of a managed static int[] field. + /// public int ReadStaticArrayInt32(string assembly, string field, int index) - => Clr.ReadArrayInt32(ResolveStaticArray(assembly, field), index); - - private uint ResolveStaticArray(string assembly, string field) - { - uint asm = Clr.FindAssembly(assembly); - if (asm == 0 || !Clr.TryResolveStaticSlot(asm, field, out int slot)) - { - throw new InvalidOperationException($"Static array '{field}' not found/ready in '{assembly}'."); - } - - uint arrayObject = Clr.ReadStatic(asm, slot).Raw; // the static cell holds the array reference - if (arrayObject == 0) - { - throw new InvalidOperationException($"Static array '{field}' is null."); - } - - return arrayObject; - } - - // Watches every Execute_IL entry; flags when the running frame's method matches the target. The - // CLR_RT_StackFrame& is the call argument, so it sits in R0 or R1 — check both. - private sealed class MethodWatch(CortexM0Plus cpu, ClrInspector clr, uint executeIl, string fqn) : IProfilingObserver - { - public bool Hit { get; private set; } - public long HitCycle { get; private set; } = -1; - - public void OnInstruction(uint pc, ushort opcode, long cycles) - { - if (Hit || pc != executeIl) - { - return; - } - - if (clr.MethodAt(cpu.Registers.R1) == fqn || clr.MethodAt(cpu.Registers.R0) == fqn) - { - Hit = true; - HitCycle = cycles; - } - } - } - - private ClrInspector? _inspector; - - /// Reads managed CLR state out of the emulator (needs g_CLR_RT_TypeSystem in the manifest). - public ClrInspector Clr => _inspector ??= - new ClrInspector(new Rp2040ClrMemory(Pico.Rp2040), Firmware.ResolveSymbol("g_CLR_RT_TypeSystem")); - - // ---- profiling: heap (dotMemory-style) + call counts (dotTrace-style) ---- - - /// - /// Snapshots the managed heap — used/free bytes and a per-kind object breakdown — for memory - /// benchmarking. Capture two and call after.Since(before) to see what an operation allocated. - /// Needs g_CLR_RT_ExecutionEngine in the firmware manifest; use the overload otherwise. - /// - public ClrInspector.HeapSnapshot CaptureHeap() => Clr.CaptureHeap(Firmware.ResolveSymbol("g_CLR_RT_ExecutionEngine")); + => Session.ReadStaticArrayInt32(assembly, field, index); - /// Snapshots the managed heap using an explicit g_CLR_RT_ExecutionEngine address. - public ClrInspector.HeapSnapshot CaptureHeap(uint executionEngineAddress) => Clr.CaptureHeap(executionEngineAddress); + /// + public ClrInspector.HeapValue ReadStatic(string assembly, string field) => Session.ReadStatic(assembly, field); - /// A call-count profile: total instructions/cycles over a run and how often each managed method was entered. - public sealed record CallProfile(long Instructions, long Cycles, IReadOnlyDictionary CallCounts); + /// + public int ReadStaticInt32(string assembly, string field) => Session.ReadStaticInt32(assembly, field); - /// - /// Runs the CLR (profiled) until holds, tallying every managed method entry - /// (each Execute_IL) by "Assembly!Method" and counting instructions/cycles — a dotTrace-style - /// "what ran, and how often". Needs Execute_IL in the manifest. - /// - public CallProfile ProfileCalls(Func until, long maxInstructions = 200_000_000) - { - uint executeIl = Firmware.ResolveSymbol("Execute_IL"); - var prof = new CallProfiler(Pico.Cpu, Clr, executeIl); - - long remaining = maxInstructions; - while (remaining > 0 && !until()) - { - int chunk = (int)Math.Min(remaining, 1_000_000); - Pico.Rp2040.RunProfiled(chunk, prof); - remaining -= chunk; - } - - return new CallProfile(prof.Instructions, prof.CycleSpan, prof.Calls); - } - - // Tallies Execute_IL entries by method (the CLR_RT_StackFrame& is the call arg → R0 or R1) and - // counts executed instructions / cycle span over the profiled window. - private sealed class CallProfiler(CortexM0Plus cpu, ClrInspector clr, uint executeIl) : IProfilingObserver - { - private readonly Dictionary _calls = new(); - private long _firstCycle = -1; - - public long Instructions { get; private set; } - public long CycleSpan { get; private set; } - public IReadOnlyDictionary Calls => _calls; - - public void OnInstruction(uint pc, ushort opcode, long cycles) - { - Instructions++; - if (_firstCycle < 0) - { - _firstCycle = cycles; - } - - CycleSpan = cycles - _firstCycle; - - if (pc != executeIl) - { - return; - } - - string method = clr.MethodAt(cpu.Registers.R1); - if (method.Length == 0) - { - method = clr.MethodAt(cpu.Registers.R0); - } - - if (method.Length != 0) - { - _calls.TryGetValue(method, out int n); - _calls[method] = n + 1; - } - } - } - - /// Reads a managed static field's cell (data type + raw value) by name (assembly must be loaded). - public ClrInspector.HeapValue ReadStatic(string assembly, string field) - { - uint asm = Clr.FindAssembly(assembly); - if (asm == 0) - { - throw new InvalidOperationException($"Assembly '{assembly}' is not loaded yet."); - } - - if (!Clr.TryResolveStaticSlot(asm, field, out int slot)) - { - throw new InvalidOperationException($"Static field '{field}' not found/ready in '{assembly}'."); - } - - return Clr.ReadStatic(asm, slot); - } - - /// Reads a managed static int field by name (the assembly must already be loaded). - public int ReadStaticInt32(string assembly, string field) => ReadStatic(assembly, field).AsInt32; + /// + public long ReadStaticInt64(string assembly, string field) => Session.ReadStaticInt64(assembly, field); - /// Reads a managed static long field by name (its full 64-bit value). - public long ReadStaticInt64(string assembly, string field) - { - uint asm = Clr.FindAssembly(assembly); - if (asm == 0 || !Clr.TryResolveStaticSlot(asm, field, out int slot)) - { - throw new InvalidOperationException($"Static field '{field}' not found/ready in '{assembly}'."); - } - - return Clr.ReadStaticInt64(asm, slot); - } - - /// - /// Reads an instance field by name from the object held in a static reference field — e.g. the - /// of type on the object in - /// . - /// + /// public ClrInspector.HeapValue ReadInstance(string assembly, string staticObjectField, string typeName, string instanceField) - { - uint asm = Clr.FindAssembly(assembly); - if (asm == 0 || !Clr.TryResolveStaticSlot(asm, staticObjectField, out int objSlot)) - { - throw new InvalidOperationException($"Static field '{staticObjectField}' not found/ready in '{assembly}'."); - } - - uint obj = Clr.ReadStatic(asm, objSlot).Raw; // the static cell holds the object reference - if (obj == 0) - { - throw new InvalidOperationException($"Static object '{staticObjectField}' is null."); - } - - if (!Clr.TryResolveInstanceSlot(asm, typeName, instanceField, out int fieldSlot)) - { - throw new InvalidOperationException($"Instance field '{typeName}.{instanceField}' not found in '{assembly}'."); - } - - return Clr.ReadInstance(obj, fieldSlot); - } + => Session.ReadInstance(assembly, staticObjectField, typeName, instanceField); - /// Reads an int instance field by name from the object in a static reference field. + /// public int ReadInstanceInt32(string assembly, string staticObjectField, string typeName, string instanceField) - => ReadInstance(assembly, staticObjectField, typeName, instanceField).AsInt32; + => Session.ReadInstanceInt32(assembly, staticObjectField, typeName, instanceField); - /// - /// Runs the CLR until a managed static field satisfies . The assembly - /// is loaded and the field resolved lazily (once the CLR has deployed it), then read at each slice - /// boundary. Returns false if the budget is exhausted first. - /// + /// public bool RunUntilStatic( string assembly, string field, Func predicate, long maxInstructions = 200_000_000, int slice = 100_000) - { - // Resolve fresh each time: the CLR may relocate the assembly object and (re)allocate its - // statics during load, so a cached pointer/slot can go stale. - bool Satisfied() - { - uint asm = Clr.FindAssembly(assembly); - return asm != 0 - && Clr.TryResolveStaticSlot(asm, field, out int slot) - && predicate(Clr.ReadStatic(asm, slot)); - } - - long ran = 0; - while (ran < maxInstructions && !Satisfied()) - { - Pico.RunInstructions(slice); - ran += slice; - } - - return Satisfied(); - } + => Session.RunUntilStatic(assembly, field, predicate, maxInstructions, slice); public void Dispose() => Pico.Dispose(); - - // Stops at the first instruction whose PC equals the target (the profiling observer sees every - // executed instruction's PC with the Thumb bit already stripped). - private sealed class PcWatch(uint target) : IProfilingObserver - { - public bool Hit { get; private set; } - public long HitCycle { get; private set; } = -1; - - public void OnInstruction(uint pc, ushort opcode, long cycles) - { - if (!Hit && pc == target) - { - Hit = true; - HitCycle = cycles; - } - } - } } diff --git a/src/RP2040Sharp.NanoFramework.TestKit/RP2040Sharp.NanoFramework.TestKit.csproj b/src/RP2040Sharp.NanoFramework.TestKit/RP2040Sharp.NanoFramework.TestKit.csproj index f7555f2..60d98e5 100644 --- a/src/RP2040Sharp.NanoFramework.TestKit/RP2040Sharp.NanoFramework.TestKit.csproj +++ b/src/RP2040Sharp.NanoFramework.TestKit/RP2040Sharp.NanoFramework.TestKit.csproj @@ -28,7 +28,7 @@ - + diff --git a/src/RP2040Sharp.NanoFramework.TestKit/Rp2040ClrHost.cs b/src/RP2040Sharp.NanoFramework.TestKit/Rp2040ClrHost.cs new file mode 100644 index 0000000..d61f07c --- /dev/null +++ b/src/RP2040Sharp.NanoFramework.TestKit/Rp2040ClrHost.cs @@ -0,0 +1,62 @@ +using NanoFramework.Clr; +using RP2040.Core.Cpu; +using RP2040.TestKit.Boards; + +namespace RP2040Sharp.NanoFramework.TestKit; + +/// +/// Adapts an RP2040Sharp (plus its for symbol +/// resolution) to the chip-agnostic , so the shared can +/// drive and read this emulator. This is the one RP2040-specific seam; the run/snapshot/profile logic +/// lives in NanoFramework.Clr.Core. +/// +public sealed class Rp2040ClrHost : IClrHost +{ + private readonly PicoSimulation _pico; + private readonly NanoFirmware _firmware; + + public Rp2040ClrHost(PicoSimulation pico, NanoFirmware firmware) + { + _pico = pico; + _firmware = firmware; + } + + public uint ReadWord(uint address) => _pico.Rp2040.Bus.ReadWord(address); + + public ushort ReadHalfWord(uint address) => _pico.Rp2040.Bus.ReadHalfWord(address); + + public byte ReadByte(uint address) => _pico.Rp2040.Bus.ReadByte(address); + + public uint Pc => _pico.Cpu.Registers.PC; + + public long InstructionCount => _pico.InstructionCount; + + public bool IsLockedUp => _pico.Cpu.IsLockedUp; + + public uint ResolveSymbol(string name) => _firmware.ResolveSymbol(name); + + public uint ArgRegister(int index) => index switch + { + 0 => _pico.Cpu.Registers.R0, + 1 => _pico.Cpu.Registers.R1, + 2 => _pico.Cpu.Registers.R2, + 3 => _pico.Cpu.Registers.R3, + _ => _pico.Cpu.Registers.R0, + }; + + public void RunInstructions(long count) => _pico.RunInstructions((int)count); + + public void RunProfiled(long count, IClrObserver observer) + => _pico.Rp2040.RunProfiled((int)count, new Bridge(observer)); + + // Forwards the emulator's per-instruction hook to the chip-agnostic observer (drops the opcode, + // which the core observers don't use). + private sealed class Bridge : IProfilingObserver + { + private readonly IClrObserver _observer; + + public Bridge(IClrObserver observer) => _observer = observer; + + public void OnInstruction(uint pc, ushort opcode, long cycles) => _observer.OnInstruction(pc, cycles); + } +} From 0872a489b7fe3e1abb6771bc3f86bcda36251e51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Montiel=20Cardona?= Date: Thu, 25 Jun 2026 17:50:15 -0600 Subject: [PATCH 03/10] Provide the RAM window in the adapters; Rp2040ClrHost reuses Rp2040ClrMemory Rp2040ClrMemory exposes RamStart/RamEnd (RP2040 SRAM); Rp2040ClrHost composes it instead of repeating the bus reads. --- .../Rp2040ClrHost.cs | 13 ++++++++++--- .../Rp2040ClrMemory.cs | 5 +++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/RP2040Sharp.NanoFramework.TestKit/Rp2040ClrHost.cs b/src/RP2040Sharp.NanoFramework.TestKit/Rp2040ClrHost.cs index d61f07c..9e27c59 100644 --- a/src/RP2040Sharp.NanoFramework.TestKit/Rp2040ClrHost.cs +++ b/src/RP2040Sharp.NanoFramework.TestKit/Rp2040ClrHost.cs @@ -14,18 +14,25 @@ public sealed class Rp2040ClrHost : IClrHost { private readonly PicoSimulation _pico; private readonly NanoFirmware _firmware; + private readonly Rp2040ClrMemory _memory; public Rp2040ClrHost(PicoSimulation pico, NanoFirmware firmware) { _pico = pico; _firmware = firmware; + _memory = new Rp2040ClrMemory(pico.Rp2040); } - public uint ReadWord(uint address) => _pico.Rp2040.Bus.ReadWord(address); + // Memory access (and the RAM window) come from the shared Rp2040ClrMemory — no duplicated bus reads. + public uint ReadWord(uint address) => _memory.ReadWord(address); - public ushort ReadHalfWord(uint address) => _pico.Rp2040.Bus.ReadHalfWord(address); + public ushort ReadHalfWord(uint address) => _memory.ReadHalfWord(address); - public byte ReadByte(uint address) => _pico.Rp2040.Bus.ReadByte(address); + public byte ReadByte(uint address) => _memory.ReadByte(address); + + public uint RamStart => _memory.RamStart; + + public uint RamEnd => _memory.RamEnd; public uint Pc => _pico.Cpu.Registers.PC; diff --git a/src/RP2040Sharp.NanoFramework.TestKit/Rp2040ClrMemory.cs b/src/RP2040Sharp.NanoFramework.TestKit/Rp2040ClrMemory.cs index 70d4731..35ba75d 100644 --- a/src/RP2040Sharp.NanoFramework.TestKit/Rp2040ClrMemory.cs +++ b/src/RP2040Sharp.NanoFramework.TestKit/Rp2040ClrMemory.cs @@ -19,4 +19,9 @@ public sealed class Rp2040ClrMemory : IClrMemory public ushort ReadHalfWord(uint address) => _machine.Bus.ReadHalfWord(address); public byte ReadByte(uint address) => _machine.Bus.ReadByte(address); + + // RP2040 SRAM: 264 KB at 0x20000000 (the striped main banks + scratch). + public uint RamStart => 0x20000000; + + public uint RamEnd => 0x20042000; } From 55d50cff1bf717c55a4fe093b0db25b280a5f746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Montiel=20Cardona?= Date: Thu, 25 Jun 2026 20:59:59 -0600 Subject: [PATCH 04/10] Load the manifest's 'layout' section so struct offsets resolve like symbols The profiler's ClrLayout is now built from these DWARF-derived offsets (with a per-field fallback), so it adapts to the firmware build instead of hardcoding a nanoCLR version's struct layout. --- src/RP2040Sharp.NanoFramework.TestKit/NanoFirmware.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/RP2040Sharp.NanoFramework.TestKit/NanoFirmware.cs b/src/RP2040Sharp.NanoFramework.TestKit/NanoFirmware.cs index ee07537..8b81eaa 100644 --- a/src/RP2040Sharp.NanoFramework.TestKit/NanoFirmware.cs +++ b/src/RP2040Sharp.NanoFramework.TestKit/NanoFirmware.cs @@ -145,6 +145,7 @@ private static (IReadOnlyDictionary Checksums, IReadOnlyDictionary using var doc = JsonDocument.Parse(File.ReadAllText(manifestPath)); ReadHexMap(doc.RootElement, "nativeChecksums", checksums); ReadHexMap(doc.RootElement, "symbols", symbols); + ReadHexMap(doc.RootElement, "layout", symbols); // struct field offsets, resolved by name like symbols return (checksums, symbols); } From eeea7a36223d6a2d11dc53d613aa6f368e107012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Montiel=20Cardona?= Date: Thu, 25 Jun 2026 22:10:10 -0600 Subject: [PATCH 05/10] Derive firmware symbols, layout and checksums from the nanoCLR ELF when present MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NanoFirmware.FromDirectory reads nanoCLR*.elf via FirmwareElf and falls back to firmware.manifest.json only when no ELF ships — no hand-written manifest needed. --- .../NanoFirmware.cs | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/src/RP2040Sharp.NanoFramework.TestKit/NanoFirmware.cs b/src/RP2040Sharp.NanoFramework.TestKit/NanoFirmware.cs index 8b81eaa..b69050f 100644 --- a/src/RP2040Sharp.NanoFramework.TestKit/NanoFirmware.cs +++ b/src/RP2040Sharp.NanoFramework.TestKit/NanoFirmware.cs @@ -1,5 +1,7 @@ using System.Globalization; +using System.Linq; using System.Text.Json; +using NanoFramework.Clr; using RP2040.TestKit.Boards; namespace RP2040Sharp.NanoFramework.TestKit; @@ -92,10 +94,53 @@ public static NanoFirmware FromDirectory(string directory) byte[] booter = File.ReadAllBytes(FindOne(directory, "nanoBooter")); byte[] clr = File.ReadAllBytes(FindOne(directory, "nanoCLR")); - var (checksums, symbols) = LoadManifest(Path.Combine(directory, "firmware.manifest.json")); + + // Prefer deriving everything from the nanoCLR ELF; fall back to a manifest when only the .bin ships. + string? elf = Directory.GetFiles(directory, "nanoCLR*.elf").FirstOrDefault(); + var (checksums, symbols) = elf is not null + ? FromElf(elf) + : LoadManifest(Path.Combine(directory, "firmware.manifest.json")); + return new NanoFirmware(booter, clr, checksums, symbols); } + // Symbols the test kit resolves by name; layout offsets and native checksums derive automatically. + private static readonly (string Name, string Pattern)[] WantedSymbols = + { + ("g_CLR_RT_TypeSystem", @"\bg_CLR_RT_TypeSystem$"), + ("g_CLR_RT_ExecutionEngine", @"\bg_CLR_RT_ExecutionEngine$"), + ("g_CLR_RT_GarbageCollector", @"\bg_CLR_RT_GarbageCollector$"), + ("Execute_IL", @"CLR_RT_Thread\d+Execute_IL"), + ("PioBlock.NativeAddProgram", @"Library_.*PioBlock\d+NativeAddProgram.*STATIC"), + ("c_CLR_StringTable_Data", @"c_CLR_StringTable_Data$"), + ("c_CLR_StringTable_Lookup", @"c_CLR_StringTable_Lookup$"), + }; + + private static (IReadOnlyDictionary Checksums, IReadOnlyDictionary Symbols) + FromElf(string elfPath) + { + FirmwareDescriptor d = FirmwareElf.Read(File.ReadAllBytes(elfPath), WantedSymbols); + + var symbols = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var (k, v) in d.Symbols) + { + symbols[k] = v; + } + + foreach (var (k, v) in d.Layout) + { + symbols[k] = v; + } + + var checksums = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var (k, v) in d.NativeChecksums) + { + checksums[k] = v; + } + + return (checksums, symbols); + } + /// /// Verifies every assembly the firmware declares a checksum for is present in /// with a matching .pe checksum. Throws on drift. From b48ce20ceea4c5858906ebcf4d6355bfa44bd568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Montiel=20Cardona?= Date: Thu, 25 Jun 2026 22:27:02 -0600 Subject: [PATCH 06/10] Pass only the chip-specific PIO symbol; the generic ones come from Core --- .../NanoFirmware.cs | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/RP2040Sharp.NanoFramework.TestKit/NanoFirmware.cs b/src/RP2040Sharp.NanoFramework.TestKit/NanoFirmware.cs index b69050f..38c6940 100644 --- a/src/RP2040Sharp.NanoFramework.TestKit/NanoFirmware.cs +++ b/src/RP2040Sharp.NanoFramework.TestKit/NanoFirmware.cs @@ -104,22 +104,13 @@ public static NanoFirmware FromDirectory(string directory) return new NanoFirmware(booter, clr, checksums, symbols); } - // Symbols the test kit resolves by name; layout offsets and native checksums derive automatically. - private static readonly (string Name, string Pattern)[] WantedSymbols = - { - ("g_CLR_RT_TypeSystem", @"\bg_CLR_RT_TypeSystem$"), - ("g_CLR_RT_ExecutionEngine", @"\bg_CLR_RT_ExecutionEngine$"), - ("g_CLR_RT_GarbageCollector", @"\bg_CLR_RT_GarbageCollector$"), - ("Execute_IL", @"CLR_RT_Thread\d+Execute_IL"), - ("PioBlock.NativeAddProgram", @"Library_.*PioBlock\d+NativeAddProgram.*STATIC"), - ("c_CLR_StringTable_Data", @"c_CLR_StringTable_Data$"), - ("c_CLR_StringTable_Lookup", @"c_CLR_StringTable_Lookup$"), - }; - private static (IReadOnlyDictionary Checksums, IReadOnlyDictionary Symbols) FromElf(string elfPath) { - FirmwareDescriptor d = FirmwareElf.Read(File.ReadAllBytes(elfPath), WantedSymbols); + // Generic nanoCLR symbols come from FirmwareElf.CoreSymbols; the PIO native call is the chip-specific one. + FirmwareDescriptor d = FirmwareElf.Read( + File.ReadAllBytes(elfPath), + ("PioBlock.NativeAddProgram", "PioBlock.NativeAddProgram")); var symbols = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var (k, v) in d.Symbols) From b34f2b214ea277b657ddae9b6f7550fc22600784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Montiel=20Cardona?= Date: Thu, 25 Jun 2026 23:21:51 -0600 Subject: [PATCH 07/10] Point the test kit at the renamed NfClr.Core --- src/RP2040Sharp.NanoFramework.TestKit/NanoClrHarness.cs | 4 ++-- src/RP2040Sharp.NanoFramework.TestKit/NanoFirmware.cs | 2 +- .../RP2040Sharp.NanoFramework.TestKit.csproj | 2 +- src/RP2040Sharp.NanoFramework.TestKit/Rp2040ClrHost.cs | 4 ++-- src/RP2040Sharp.NanoFramework.TestKit/Rp2040ClrMemory.cs | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/RP2040Sharp.NanoFramework.TestKit/NanoClrHarness.cs b/src/RP2040Sharp.NanoFramework.TestKit/NanoClrHarness.cs index b0b58ef..cb007b0 100644 --- a/src/RP2040Sharp.NanoFramework.TestKit/NanoClrHarness.cs +++ b/src/RP2040Sharp.NanoFramework.TestKit/NanoClrHarness.cs @@ -1,4 +1,4 @@ -using NanoFramework.Clr; +using NfClr; using RP2040.TestKit.Boards; namespace RP2040Sharp.NanoFramework.TestKit; @@ -12,7 +12,7 @@ namespace RP2040Sharp.NanoFramework.TestKit; /// /// This is the RP2040 façade over the chip-agnostic : it owns the emulator and /// the boot, and adapts them through . All the run/snapshot/profile logic -/// lives in NanoFramework.Clr.Core and is shared with the other chips' kits and the profiler. The run +/// lives in NfClr.Core and is shared with the other chips' kits and the profiler. The run /// methods drive the emulator's hook-aware execution path (the one that services the bootrom/flash /// hooks the firmware needs to boot). /// diff --git a/src/RP2040Sharp.NanoFramework.TestKit/NanoFirmware.cs b/src/RP2040Sharp.NanoFramework.TestKit/NanoFirmware.cs index 38c6940..cd5dbc2 100644 --- a/src/RP2040Sharp.NanoFramework.TestKit/NanoFirmware.cs +++ b/src/RP2040Sharp.NanoFramework.TestKit/NanoFirmware.cs @@ -1,7 +1,7 @@ using System.Globalization; using System.Linq; using System.Text.Json; -using NanoFramework.Clr; +using NfClr; using RP2040.TestKit.Boards; namespace RP2040Sharp.NanoFramework.TestKit; diff --git a/src/RP2040Sharp.NanoFramework.TestKit/RP2040Sharp.NanoFramework.TestKit.csproj b/src/RP2040Sharp.NanoFramework.TestKit/RP2040Sharp.NanoFramework.TestKit.csproj index 60d98e5..4aeb456 100644 --- a/src/RP2040Sharp.NanoFramework.TestKit/RP2040Sharp.NanoFramework.TestKit.csproj +++ b/src/RP2040Sharp.NanoFramework.TestKit/RP2040Sharp.NanoFramework.TestKit.csproj @@ -28,7 +28,7 @@ - + diff --git a/src/RP2040Sharp.NanoFramework.TestKit/Rp2040ClrHost.cs b/src/RP2040Sharp.NanoFramework.TestKit/Rp2040ClrHost.cs index 9e27c59..6c4ff1f 100644 --- a/src/RP2040Sharp.NanoFramework.TestKit/Rp2040ClrHost.cs +++ b/src/RP2040Sharp.NanoFramework.TestKit/Rp2040ClrHost.cs @@ -1,4 +1,4 @@ -using NanoFramework.Clr; +using NfClr; using RP2040.Core.Cpu; using RP2040.TestKit.Boards; @@ -8,7 +8,7 @@ namespace RP2040Sharp.NanoFramework.TestKit; /// Adapts an RP2040Sharp (plus its for symbol /// resolution) to the chip-agnostic , so the shared can /// drive and read this emulator. This is the one RP2040-specific seam; the run/snapshot/profile logic -/// lives in NanoFramework.Clr.Core. +/// lives in NfClr.Core. /// public sealed class Rp2040ClrHost : IClrHost { diff --git a/src/RP2040Sharp.NanoFramework.TestKit/Rp2040ClrMemory.cs b/src/RP2040Sharp.NanoFramework.TestKit/Rp2040ClrMemory.cs index 35ba75d..59fe66e 100644 --- a/src/RP2040Sharp.NanoFramework.TestKit/Rp2040ClrMemory.cs +++ b/src/RP2040Sharp.NanoFramework.TestKit/Rp2040ClrMemory.cs @@ -1,4 +1,4 @@ -using NanoFramework.Clr; +using NfClr; using RP2040.Peripherals; namespace RP2040Sharp.NanoFramework.TestKit; @@ -6,7 +6,7 @@ namespace RP2040Sharp.NanoFramework.TestKit; /// /// Adapts an RP2040Sharp machine's bus to the chip-agnostic so the shared /// can read this emulator's RAM. This is the one RP2040-specific seam the -/// CLR walker needs; the rest of the introspection lives in NanoFramework.Clr.Core. +/// CLR walker needs; the rest of the introspection lives in NfClr.Core. /// public sealed class Rp2040ClrMemory : IClrMemory { From 672ea036235a69592c8efab302a578287f400f7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Montiel=20Cardona?= Date: Thu, 25 Jun 2026 23:49:14 -0600 Subject: [PATCH 08/10] ci(publish): migrate to NuGet Trusted Publishing (OIDC) Replace the long-lived NUGET_API_KEY with NuGet/login@v1: the workflow mints a short-lived key from the GitHub OIDC token at push time (permissions: id-token: write). Requires a Trusted Publishing policy on nuget.org (Repository Owner: PyMCU, Repository: RP2040Sharp, Workflow File: publish.yml) and a repo secret NUGET_USER (nuget.org profile name). The NUGET_API_KEY secret can be deleted once verified. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/publish.yml | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a8d0690..c840086 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -5,7 +5,13 @@ name: Publish NuGet # stable release version, any other ref produces a preview version derived from # the last tag + commit height. Packages are always built and uploaded as a # workflow artifact; they are pushed to nuget.org only for `v*` tags (or when a -# manual run sets push_public=true). Compatible with GitHub and Gitea runners. +# manual run sets push_public=true). +# +# Publishing uses NuGet Trusted Publishing (OIDC) — no long-lived API key. A short-lived +# key is minted at push time via NuGet/login from the GitHub OIDC token. This requires: +# 1) a Trusted Publishing policy on nuget.org (Repository Owner: PyMCU, +# Repository: RP2040Sharp, Workflow File: publish.yml), and +# 2) a repo secret NUGET_USER set to your nuget.org profile name (NOT your email). # ───────────────────────────────────────────────────────────────────────────── on: push: @@ -21,6 +27,9 @@ on: jobs: publish: runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # required for the GitHub OIDC token (Trusted Publishing) steps: # ── Checkout ──────────────────────────────────────────────────────────── @@ -82,19 +91,21 @@ jobs: --no-build --output ./nupkgs + # ── Trusted Publishing: mint a short-lived API key from the OIDC token ──── + - name: NuGet login (OIDC → temporary API key) + if: ${{ startsWith(github.ref, 'refs/tags/v') || github.event.inputs.push_public == 'true' }} + uses: NuGet/login@v1 + id: login + with: + user: ${{ secrets.NUGET_USER }} + # ── Push to nuget.org (tags, or a manual push_public run) ─────────────── - name: Push to nuget.org - env: - API_KEY: ${{ secrets.NUGET_API_KEY }} - if: > - ${{ github.server_url == 'https://github.com' && - env.API_KEY != '' && - (startsWith(github.ref, 'refs/tags/v') || - github.event.inputs.push_public == 'true') }} + if: ${{ startsWith(github.ref, 'refs/tags/v') || github.event.inputs.push_public == 'true' }} run: > dotnet nuget push "./nupkgs/*.nupkg" --source https://api.nuget.org/v3/index.json - --api-key "${{ secrets.NUGET_API_KEY }}" + --api-key "${{ steps.login.outputs.NUGET_API_KEY }}" --skip-duplicate # ── Always upload the packages as an artifact ─────────────────────────── From 9ed02059126d1e1e77ff9bb322ba81bf7eecc55a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Montiel=20Cardona?= Date: Thu, 25 Jun 2026 23:53:37 -0600 Subject: [PATCH 09/10] build(nano-testkit): depend on NfClr.Core via PackageReference (1.0.0-beta.1) Now that NfClr.Core ships on nuget.org, replace the cross-repo ProjectReference (..\..\..\NfClr\src\NfClr.Core) with a PackageReference. The published TestKit now declares a clean NfClr.Core dependency instead of needing the sibling repo checked out. Prepares the extract-clr-core branch for the 1.0.1-beta.6 release. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../RP2040Sharp.NanoFramework.TestKit.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/RP2040Sharp.NanoFramework.TestKit/RP2040Sharp.NanoFramework.TestKit.csproj b/src/RP2040Sharp.NanoFramework.TestKit/RP2040Sharp.NanoFramework.TestKit.csproj index 4aeb456..fa61df5 100644 --- a/src/RP2040Sharp.NanoFramework.TestKit/RP2040Sharp.NanoFramework.TestKit.csproj +++ b/src/RP2040Sharp.NanoFramework.TestKit/RP2040Sharp.NanoFramework.TestKit.csproj @@ -27,8 +27,8 @@ - + the RP2040 IClrMemory adapter; the CLR knowledge lives once in the shared NfClr.Core package. --> + From 1c9445c09ec74f0adfaa2f823efb119b7160027d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Montiel=20Cardona?= Date: Thu, 25 Jun 2026 23:57:14 -0600 Subject: [PATCH 10/10] docs(changelog): 1.0.1-beta.6 (NfClr.Core extraction + Trusted Publishing) Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19fdafd..7f9fb50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.0.1-beta.6] - 2026-06-26 + +### Changed + +- **Extracted the chip-agnostic nanoCLR introspection into the shared + [NfClr.Core](https://github.com/begeistert/NfClr) package.** `ClrInspector`, `ClrLayout`, + the heap walk and `ClrSession` now live once in `NfClr.Core` (`1.0.0-beta.1`); + `RP2040Sharp.NanoFramework.TestKit` keeps only the RP2040 `IClrMemory`/`IClrHost` adapter + (`Rp2040ClrHost` / `Rp2040ClrMemory`) and depends on `NfClr.Core` via **PackageReference**. + `NanoClrHarness` is now a thin facade over `ClrSession`. Firmware symbols, layout and + checksums derive from the nanoCLR ELF/DWARF when present. +- **Publishing migrated to NuGet Trusted Publishing (OIDC)** — no long-lived API key. + ## [1.0.1-beta.5] - 2026-06-25 ### Changed @@ -153,7 +166,8 @@ First public release candidate. A high-performance RP2040 emulator in modern C# - Flash programming uses C# hooks rather than the SSI XIP hardware path. - USB host support is CDC-only (sufficient for the MicroPython REPL). -[Unreleased]: https://github.com/PyMCU/RP2040Sharp/compare/v1.0.1-beta.5...HEAD +[Unreleased]: https://github.com/PyMCU/RP2040Sharp/compare/v1.0.1-beta.6...HEAD +[1.0.1-beta.6]: https://github.com/PyMCU/RP2040Sharp/compare/v1.0.1-beta.5...v1.0.1-beta.6 [1.0.1-beta.5]: https://github.com/PyMCU/RP2040Sharp/compare/v1.0.1-beta.4...v1.0.1-beta.5 [1.0.1-beta.4]: https://github.com/PyMCU/RP2040Sharp/compare/v1.0.1-beta.3...v1.0.1-beta.4 [1.0.1-beta.3]: https://github.com/PyMCU/RP2040Sharp/compare/v1.0.1-beta.2...v1.0.1-beta.3