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 ───────────────────────────
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
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/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 dc5bbc2..cb007b0 100644
--- a/src/RP2040Sharp.NanoFramework.TestKit/NanoClrHarness.cs
+++ b/src/RP2040Sharp.NanoFramework.TestKit/NanoClrHarness.cs
@@ -1,19 +1,20 @@
-using RP2040.Core.Cpu;
+using NfClr;
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 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).
///
public sealed class NanoClrHarness : IDisposable
{
@@ -23,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));
}
///
@@ -47,247 +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;
+ /// The symbol last reached by (null if none).
+ public string? LastReachedSymbol => Session.LastReachedSymbol;
- public bool IsLockedUp => Pico.Cpu.IsLockedUp;
+ /// The cycle count when was reached (-1 if none).
+ public long LastReachedCycle => Session.LastReachedCycle;
- public long InstructionCount => Pico.InstructionCount;
+ /// The current CPU program counter.
+ public uint Pc => Session.Pc;
- ///
- /// 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;
- }
+ public bool IsLockedUp => Session.IsLockedUp;
- return ready();
- }
+ public long InstructionCount => Session.InstructionCount;
- ///
- /// 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);
-
- 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(Pico.Rp2040, Firmware.ResolveSymbol("g_CLR_RT_TypeSystem"));
-
- /// 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);
- }
+ => Session.ReadStaticArrayInt32(assembly, field, index);
- /// 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 ClrInspector.HeapValue ReadStatic(string assembly, string field) => Session.ReadStatic(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}'.");
- }
+ ///
+ public int ReadStaticInt32(string assembly, string field) => Session.ReadStaticInt32(assembly, field);
- return Clr.ReadStaticInt64(asm, slot);
- }
+ ///
+ public long ReadStaticInt64(string assembly, string field) => Session.ReadStaticInt64(assembly, field);
- ///
- /// 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/NanoFirmware.cs b/src/RP2040Sharp.NanoFramework.TestKit/NanoFirmware.cs
index ee07537..cd5dbc2 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 NfClr;
using RP2040.TestKit.Boards;
namespace RP2040Sharp.NanoFramework.TestKit;
@@ -92,10 +94,44 @@ 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);
}
+ private static (IReadOnlyDictionary Checksums, IReadOnlyDictionary Symbols)
+ FromElf(string elfPath)
+ {
+ // 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)
+ {
+ 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.
@@ -145,6 +181,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);
}
diff --git a/src/RP2040Sharp.NanoFramework.TestKit/RP2040Sharp.NanoFramework.TestKit.csproj b/src/RP2040Sharp.NanoFramework.TestKit/RP2040Sharp.NanoFramework.TestKit.csproj
index 09e13d8..fa61df5 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/Rp2040ClrHost.cs b/src/RP2040Sharp.NanoFramework.TestKit/Rp2040ClrHost.cs
new file mode 100644
index 0000000..6c4ff1f
--- /dev/null
+++ b/src/RP2040Sharp.NanoFramework.TestKit/Rp2040ClrHost.cs
@@ -0,0 +1,69 @@
+using NfClr;
+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 NfClr.Core.
+///
+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);
+ }
+
+ // 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) => _memory.ReadHalfWord(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;
+
+ 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);
+ }
+}
diff --git a/src/RP2040Sharp.NanoFramework.TestKit/Rp2040ClrMemory.cs b/src/RP2040Sharp.NanoFramework.TestKit/Rp2040ClrMemory.cs
new file mode 100644
index 0000000..59fe66e
--- /dev/null
+++ b/src/RP2040Sharp.NanoFramework.TestKit/Rp2040ClrMemory.cs
@@ -0,0 +1,27 @@
+using NfClr;
+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 NfClr.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);
+
+ // RP2040 SRAM: 264 KB at 0x20000000 (the striped main banks + scratch).
+ public uint RamStart => 0x20000000;
+
+ public uint RamEnd => 0x20042000;
+}