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; +}