Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 30 additions & 5 deletions .github/workflows/codegen-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@ on:
- 'tools/codegen/**'
- 'src/Framework/**'
- 'src/Framework.Tests/**'
- 'native/test_stub/**'
- 'native/dsp_fixture/**'
- '.github/workflows/codegen-tests.yml'
- 'Directory.Build.props'
pull_request:
paths:
- 'tools/codegen/**'
- 'src/Framework/**'
- 'src/Framework.Tests/**'
- 'native/test_stub/**'
- 'native/dsp_fixture/**'
- '.github/workflows/codegen-tests.yml'
- 'Directory.Build.props'

Expand Down Expand Up @@ -81,7 +85,7 @@ jobs:
- name: Build codegen
run: FerrumSkipMobile=true dotnet build tools/codegen/Ferrum.Codegen.csproj -c Release

- name: Run codegen on test stub header
- name: Run codegen on test stub header (ferrum_add)
run: |
FerrumSkipMobile=true dotnet run --project tools/codegen/Ferrum.Codegen.csproj \
--configuration Release \
Expand All @@ -92,11 +96,32 @@ jobs:
--class AddBindings \
--lib __Internal

- name: Verify output
- name: Verify add.h output
run: |
echo "=== Generated bindings ==="
echo "=== Generated bindings (add.h) ==="
cat /tmp/AddBindings.cs
# Ensure [LibraryImport] and the function name are present
grep -q "\[LibraryImport" /tmp/AddBindings.cs
grep -q "ferrum_add" /tmp/AddBindings.cs
echo "==> Smoke test passed"
echo "==> add.h smoke test passed"

- name: Run codegen on DSP fixture header (ferrum_dsp)
run: |
FerrumSkipMobile=true dotnet run --project tools/codegen/Ferrum.Codegen.csproj \
--configuration Release \
-- \
--input native/dsp_fixture/include/ferrum_dsp.h \
--output /tmp/DspBindings.cs \
--ns Ferrum.Generated \
--class DspBindings \
--lib __Internal

- name: Verify ferrum_dsp.h output
run: |
echo "=== Generated bindings (ferrum_dsp.h) ==="
cat /tmp/DspBindings.cs
grep -q "\[LibraryImport" /tmp/DspBindings.cs
grep -q "ferrum_dsp_scale" /tmp/DspBindings.cs
grep -q "ferrum_dsp_stats" /tmp/DspBindings.cs
grep -q "FerrumDspStats" /tmp/DspBindings.cs
grep -q "StructLayout" /tmp/DspBindings.cs
echo "==> ferrum_dsp.h smoke test passed"
80 changes: 55 additions & 25 deletions .github/workflows/native-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,37 @@ permissions:
jobs:
# ── iOS XCFramework ────────────────────────────────────────────────────────
ios:
name: iOS XCFramework (${{ matrix.platform }})
name: iOS XCFramework (${{ matrix.platform }}, ${{ matrix.target }})
runs-on: macos-15
strategy:
matrix:
include:
# test_stub slices
- platform: OS64
sysroot: iphoneos
label: device-arm64
target: ferrum_test_stub
- platform: SIMULATORARM64
sysroot: iphonesimulator
label: sim-arm64
target: ferrum_test_stub
- platform: SIMULATOR64
sysroot: iphonesimulator
label: sim-x86_64
target: ferrum_test_stub
# dsp_fixture slices
- platform: OS64
sysroot: iphoneos
label: dsp-device-arm64
target: ferrum_dsp_fixture
- platform: SIMULATORARM64
sysroot: iphonesimulator
label: dsp-sim-arm64
target: ferrum_dsp_fixture
- platform: SIMULATOR64
sysroot: iphonesimulator
label: dsp-sim-x86_64
target: ferrum_dsp_fixture

steps:
- uses: actions/checkout@v4
Expand All @@ -57,65 +74,78 @@ jobs:
run: |
cmake --build .build/ios/${{ matrix.label }} \
--config Release \
--target ferrum_test_stub
--target ${{ matrix.target }}

- name: Upload slice artifact
uses: actions/upload-artifact@v4
with:
name: ios-${{ matrix.label }}
path: .build/ios/${{ matrix.label }}/Release-${{ matrix.sysroot }}/libferrum_test_stub.a
path: .build/ios/${{ matrix.label }}/Release-${{ matrix.sysroot }}/lib${{ matrix.target }}.a

# Assemble the full XCFramework from the three slices
# Assemble the full XCFramework from the three slices (per target)
ios-xcframework:
name: Assemble XCFramework
name: Assemble XCFramework (${{ matrix.target }})
runs-on: macos-15
needs: ios
strategy:
matrix:
include:
- target: ferrum_test_stub
device_label: device-arm64
sim_arm64_label: sim-arm64
sim_x86_label: sim-x86_64
- target: ferrum_dsp_fixture
device_label: dsp-device-arm64
sim_arm64_label: dsp-sim-arm64
sim_x86_label: dsp-sim-x86_64
steps:
- uses: actions/checkout@v4

- uses: actions/download-artifact@v4
with:
name: ios-device-arm64
path: .slices/device
name: ios-${{ matrix.device_label }}
path: .slices/${{ matrix.target }}/device

- uses: actions/download-artifact@v4
with:
name: ios-sim-arm64
path: .slices/sim-arm64
name: ios-${{ matrix.sim_arm64_label }}
path: .slices/${{ matrix.target }}/sim-arm64

- uses: actions/download-artifact@v4
with:
name: ios-sim-x86_64
path: .slices/sim-x86_64
name: ios-${{ matrix.sim_x86_label }}
path: .slices/${{ matrix.target }}/sim-x86_64

- name: Create fat simulator library
run: |
mkdir -p .slices/${{ matrix.target }}/sim-fat
lipo -create \
.slices/sim-arm64/libferrum_test_stub.a \
.slices/sim-x86_64/libferrum_test_stub.a \
-output .slices/sim-fat/libferrum_test_stub.a
.slices/${{ matrix.target }}/sim-arm64/lib${{ matrix.target }}.a \
.slices/${{ matrix.target }}/sim-x86_64/lib${{ matrix.target }}.a \
-output .slices/${{ matrix.target }}/sim-fat/lib${{ matrix.target }}.a

- name: Create XCFramework
run: |
mkdir -p artifacts/ios
xcodebuild -create-xcframework \
-library .slices/device/libferrum_test_stub.a \
-library .slices/sim-fat/libferrum_test_stub.a \
-output artifacts/ios/libferrum_test_stub.xcframework
-library .slices/${{ matrix.target }}/device/lib${{ matrix.target }}.a \
-library .slices/${{ matrix.target }}/sim-fat/lib${{ matrix.target }}.a \
-output artifacts/ios/lib${{ matrix.target }}.xcframework

- name: Upload XCFramework
uses: actions/upload-artifact@v4
with:
name: ios-xcframework
path: artifacts/ios/libferrum_test_stub.xcframework
name: ios-xcframework-${{ matrix.target }}
path: artifacts/ios/lib${{ matrix.target }}.xcframework

# ── Android .so per ABI ────────────────────────────────────────────────────
android:
name: Android (${{ matrix.abi }})
name: Android (${{ matrix.abi }}, ${{ matrix.target }})
runs-on: ubuntu-24.04
strategy:
matrix:
abi: [arm64-v8a, armeabi-v7a, x86_64, x86]
target: [ferrum_test_stub, ferrum_dsp_fixture]

steps:
- uses: actions/checkout@v4
Expand All @@ -130,20 +160,20 @@ jobs:
run: |
NDK_ROOT="${ANDROID_SDK_ROOT}/ndk/27.2.12479018"
cmake -S native \
-B .build/android/${{ matrix.abi }} \
-B .build/android/${{ matrix.abi }}-${{ matrix.target }} \
-DCMAKE_TOOLCHAIN_FILE="${NDK_ROOT}/build/cmake/android.toolchain.cmake" \
-DANDROID_ABI=${{ matrix.abi }} \
-DANDROID_PLATFORM=android-24 \
-DCMAKE_BUILD_TYPE=Release

- name: Build
run: |
cmake --build .build/android/${{ matrix.abi }} \
cmake --build .build/android/${{ matrix.abi }}-${{ matrix.target }} \
--config Release \
--target ferrum_test_stub
--target ${{ matrix.target }}

- name: Upload .so artifact
uses: actions/upload-artifact@v4
with:
name: android-${{ matrix.abi }}
path: .build/android/${{ matrix.abi }}/libferrum_test_stub.so
name: android-${{ matrix.abi }}-${{ matrix.target }}
path: .build/android/${{ matrix.abi }}-${{ matrix.target }}/lib${{ matrix.target }}.so
9 changes: 9 additions & 0 deletions Ferrum.sln
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{A0000002
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{A0000003-0000-0000-0000-000000000003}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "templates", "templates", "{A0000004-0000-0000-0000-000000000004}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ferrum.Framework", "src\Framework\Ferrum.Framework.csproj", "{B1000001-0000-0000-0000-000000000001}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ferrum.Framework.Tests", "src\Framework.Tests\Ferrum.Framework.Tests.csproj", "{B1000002-0000-0000-0000-000000000002}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ferrum.Codegen", "tools\codegen\Ferrum.Codegen.csproj", "{C1000001-0000-0000-0000-000000000001}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ferrum.Templates", "templates\Ferrum.Templates.csproj", "{C1000002-0000-0000-0000-000000000002}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MinimalDemo", "samples\MinimalDemo\MinimalDemo.csproj", "{D1000001-0000-0000-0000-000000000001}"
EndProject
Global
Expand All @@ -35,6 +39,10 @@ Global
{C1000001-0000-0000-0000-000000000001}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C1000001-0000-0000-0000-000000000001}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C1000001-0000-0000-0000-000000000001}.Release|Any CPU.Build.0 = Release|Any CPU
{C1000002-0000-0000-0000-000000000002}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C1000002-0000-0000-0000-000000000002}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C1000002-0000-0000-0000-000000000002}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C1000002-0000-0000-0000-000000000002}.Release|Any CPU.Build.0 = Release|Any CPU
{D1000001-0000-0000-0000-000000000001}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D1000001-0000-0000-0000-000000000001}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D1000001-0000-0000-0000-000000000001}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand All @@ -44,6 +52,7 @@ Global
{B1000001-0000-0000-0000-000000000001} = {A0000001-0000-0000-0000-000000000001}
{B1000002-0000-0000-0000-000000000002} = {A0000001-0000-0000-0000-000000000001}
{C1000001-0000-0000-0000-000000000001} = {A0000002-0000-0000-0000-000000000002}
{C1000002-0000-0000-0000-000000000002} = {A0000004-0000-0000-0000-000000000004}
{D1000001-0000-0000-0000-000000000001} = {A0000003-0000-0000-0000-000000000003}
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
Expand Down
21 changes: 15 additions & 6 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,11 @@ the consuming application provides the native library.

| File / directory | Purpose |
|---|---|
| `native/CMakeLists.txt` | Top-level project; includes helpers, optionally builds the test stub |
| `native/CMakeLists.txt` | Top-level project; includes helpers, optionally builds the test fixtures |
| `native/cmake/ferrum_helpers.cmake` | `ferrum_add_static_library()` macro that sets common flags |
| `native/cmake/ios.toolchain.cmake` | Minimal iOS cross-compilation toolchain (no external deps) |
| `native/test_stub/` | Trivial `ferrum_add(int, int)` C library used to validate the pipeline |
| `native/dsp_fixture/` | `ferrum_dsp_scale(float*, int32_t, float)` + `ferrum_dsp_stats(const float*, int32_t, FerrumDspStats*)` — validates the float-buffer and struct-out-param patterns |
| `native/scripts/build_ios.sh` | Builds all three iOS slices and packages them as an XCFramework |
| `native/scripts/build_android.sh` | Builds `.so` for all four Android ABIs using the NDK toolchain |

Expand Down Expand Up @@ -135,9 +136,14 @@ definitions.
(source-generated at build time) instead of `[DllImport]` (which uses
reflection at runtime). No `MarshalAs` attributes are emitted.

3. **Simple, dependency-free parser.** The parser is a regex/tokenizer
approach — not a full C frontend. It handles the subset of C headers
needed for P/Invoke: function prototypes and POD structs.
3. **Simple, dependency-free parser.** The parser is a strict tokenizer —
not a full C frontend. It handles the subset of C headers needed for
P/Invoke: function prototypes and POD structs. Anything it does not
recognise is rejected immediately (see constraint 1). A full libclang
integration was considered and rejected: it would add ~40–50 MB of native
binary dependencies to the dotnet tool package without materially improving
correctness within the blittable-only constraint. See
[open-questions.md](open-questions.md) item 5 for the full rationale.

### Supported C types

Expand Down Expand Up @@ -191,5 +197,8 @@ Indicators of scope creep (flag these in code review):
- A function or type name references a specific application domain
(audio, video, ML model name, etc.)
- The codegen tool learns about specific library ABIs by name
- Any native code beyond the trivial `ferrum_add()` test stub is added to
`native/test_stub/`
- Any native code beyond the trivial test fixtures is added to `native/`

The in-tree test fixtures (`test_stub/`, `dsp_fixture/`) are explicitly exempt
from the scope rule — they exist solely to prove the framework plumbing works
with different parameter patterns, not to implement any application logic.
57 changes: 57 additions & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,63 @@ dotnet build samples/MinimalDemo/MinimalDemo.csproj -f net9.0-ios -c Release

---

## NativeAOT on a real iOS device — manual verification

> **TODO (issue):** Automated CI verification on a physical device is not
> currently wired up. Physical device builds require a paid Apple Developer
> account and provisioning profiles, which cannot be stored in a public
> repository. Track this as a known gap until a self-hosted runner with a
> provisioned device is available.

Until automated CI coverage is in place, run the following steps manually
whenever `NativeBuffer<T>` or the generated bindings change:

### Prerequisites

- macOS with Xcode 15+ and a valid iOS Developer provisioning profile
- An iOS device running iOS 15.0 or later
- .NET SDK 9.0 with the MAUI workload (`dotnet workload install maui`)

### Steps

1. **Build the native fixture:**

```bash
./native/scripts/build_ios.sh --target ferrum_test_stub
./native/scripts/build_ios.sh --target ferrum_dsp_fixture
```

2. **Reference the XCFrameworks** in `samples/MinimalDemo/MinimalDemo.csproj`
(uncomment the `<NativeReference>` items).

3. **Build in Release/NativeAOT mode for a physical device:**

```bash
dotnet build samples/MinimalDemo/MinimalDemo.csproj \
-f net9.0-ios \
-c Release \
-p:RuntimeIdentifier=ios-arm64 \
-p:_BundlerExecutable=mtouch
```

4. **Install and run on the device.** Open Xcode → Window → Devices and
Simulators, drag the `.ipa` to your device, and tap it.

5. **Verify the following under NativeAOT** (check Xcode console output):
- `NativeBuffer<float>` pins memory and returns a non-null `TypedPointer`.
- `ferrum_add(3, 4)` returns `7`.
- `ferrum_dsp_scale` operates in-place on a float buffer without crashing.
- `ferrum_dsp_stats` fills a `FerrumDspStats` struct with correct values.
- No interpreter fallback messages appear (look for `Falling back to interpreter`
— there should be none).
- The app does not throw `MissingRuntimeArtifactException` or any
reflection-related exception.

6. **Record the result** in the PR description, including the device model,
iOS version, and .NET SDK version used.

---

## CMake integration in your own project

If your native library has its own `CMakeLists.txt`, you can use the Ferrum
Expand Down
Loading
Loading