Skip to content

Commit 70916cd

Browse files
authored
[skills] Update macios-binding-creator skill with new learnings (#24947)
1 parent 7175472 commit 70916cd

3 files changed

Lines changed: 169 additions & 25 deletions

File tree

.agents/skills/macios-binding-creator/SKILL.md

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,34 @@ NSString ScheduleRequestedNotification { get; }
9494
9595
> **NEVER** use `string.Empty` — use `""`. Never use `Array.Empty<T>()` — use `[]`.
9696
97+
> **NEVER** use non-blittable types (`bool`, `char`) as backing fields in structs. Use `byte` (for `bool`) and `ushort`/`short` (for `char`) with property accessors. See [references/binding-patterns.md](references/binding-patterns.md) for the correct pattern.
98+
99+
> **NEVER** use `XAMCORE_5_0` for new code. `XAMCORE_5_0` is only for fixing breaking API changes on existing types that shipped in prior releases.
100+
101+
> **NEVER** use `#pragma warning disable 0169` for struct fields. Instead, wrap public methods and properties inside `#if !COREBUILD` (but NOT fields — bgen needs to know the struct size).
102+
97103
> ⚠️ Place a space before parentheses and brackets: `Foo ()`, `Bar (1, 2)`, `myarray [0]`.
98104
105+
> ⚠️ Method names should follow .NET naming conventions — use verb-based names, not direct Objective-C selector translations (e.g., `BuildMenu` not `MenuWithContents`).
106+
99107
> ⚠️ For in depth binding patterns and conventions See [references/binding-patterns.md](references/binding-patterns.md)
100108
109+
### Step 4b: Platform Exclusion Patterns for Manual Types
110+
111+
When a manually coded type (struct, extension, etc.) is not available on a specific platform (e.g., tvOS), you must handle compilation on that platform:
112+
113+
1. In the manual code file (`src/FrameworkName/MyStruct.cs`), wrap the struct body with `#if !TVOS`
114+
2. Add `[UnsupportedOSPlatform ("tvos")]` on the struct
115+
3. In the API definition file (`src/frameworkname.cs`), add a type alias at the top so compilation succeeds:
116+
117+
```csharp
118+
#if TVOS
119+
using MyStruct = Foundation.NSObject;
120+
#endif
121+
```
122+
123+
The `[NoTV]` attribute on the API definition interface ensures the type won't appear in the final tvOS assembly, while the alias prevents compilation errors from method signatures that reference the struct.
124+
101125
### Step 5: Build
102126

103127
```bash
@@ -106,6 +130,8 @@ make -C src build
106130

107131
Fix any compilation errors before proceeding. Builds can take up to 60 minutes — do not timeout early.
108132

133+
> ⚠️ **Stale build artifacts**: If you encounter unexpected test failures (false "pre-existing" failures, segfaults in unrelated types), run a full `make all && make install` to clear stale `_build/` artifacts before re-testing.
134+
109135
### Step 6: Validate with Tests
110136

111137
Run all three test suites. **Run them sequentially, not in parallel.**
@@ -132,26 +158,49 @@ make -C tests/cecil-tests run-tests
132158
**IMPORTANT:** Clean shared obj directories before each platform to avoid NETSDK1005 errors:
133159

134160
```bash
135-
# iOS
161+
# iOS — build, then run via mlaunch directly for reliable output capture
136162
rm -rf tests/common/Touch.Unit/Touch.Client/dotnet/obj tests/common/MonoTouch.Dialog/obj
137-
make -C tests/introspection/dotnet clean-ios build-ios run-ios
138-
139-
# tvOS
163+
make -C tests/introspection/dotnet/iOS clean
164+
make -C tests/introspection/dotnet build-ios
165+
# Get the app path and run via mlaunch directly:
166+
APP_PATH=$(make -C tests/introspection/dotnet/iOS print-executable | sed 's|/introspection$||')
167+
SIMCTL_CHILD_NUNIT_AUTOSTART=true \
168+
SIMCTL_CHILD_NUNIT_AUTOEXIT=true \
169+
$DOTNET_DESTDIR/Microsoft.iOS.Sdk/tools/bin/mlaunch \
170+
--launchsim "$APP_PATH" \
171+
--device :v2:runtime=com.apple.CoreSimulator.SimRuntime.iOS-26-4,devicetype=com.apple.CoreSimulator.SimDeviceType.iPhone-16-Pro \
172+
--wait-for-exit:true --
173+
174+
# tvOS — same approach as iOS
140175
rm -rf tests/common/Touch.Unit/Touch.Client/dotnet/obj tests/common/MonoTouch.Dialog/obj
141-
make -C tests/introspection/dotnet clean-tvos build-tvos run-tvos
176+
make -C tests/introspection/dotnet/tvOS clean
177+
make -C tests/introspection/dotnet build-tvos
178+
APP_PATH=$(make -C tests/introspection/dotnet/tvOS print-executable | sed 's|/introspection$||')
179+
SIMCTL_CHILD_NUNIT_AUTOSTART=true \
180+
SIMCTL_CHILD_NUNIT_AUTOEXIT=true \
181+
$DOTNET_DESTDIR/Microsoft.tvOS.Sdk/tools/bin/mlaunch \
182+
--launchsim "$APP_PATH" \
183+
--device :v2:runtime=com.apple.CoreSimulator.SimRuntime.tvOS-26-4,devicetype=com.apple.CoreSimulator.SimDeviceType.Apple-TV-4K-3rd-generation-4K \
184+
--wait-for-exit:true --
142185

143186
# macOS (use run-bare for direct execution with captured output)
144187
rm -rf tests/common/Touch.Unit/Touch.Client/dotnet/obj tests/common/MonoTouch.Dialog/obj
145-
make -C tests/introspection/dotnet clean-macOS build-macOS run-bare-macOS
188+
make -C tests/introspection/dotnet/macOS clean build
189+
make -C tests/introspection/dotnet/macOS run-bare
146190

147191
# MacCatalyst (use run-bare for direct execution with captured output)
148192
rm -rf tests/common/Touch.Unit/Touch.Client/dotnet/obj tests/common/MonoTouch.Dialog/obj
149-
make -C tests/introspection/dotnet clean-MacCatalyst build-MacCatalyst run-bare-MacCatalyst
193+
make -C tests/introspection/dotnet/MacCatalyst clean build
194+
make -C tests/introspection/dotnet/MacCatalyst run-bare
150195
```
151196

152-
> ⚠️ **macOS/MacCatalyst:** Use `make run-bare` (not `make run`) — `make run` launches the app without waiting or capturing stdout. `run-bare` runs the executable directly to capture test output.
197+
> ⚠️ **iOS/tvOS output capture:** `make run-ios`/`run-tvos` uses `dotnet build -t:Run` which does NOT reliably capture the app's stdout. The `com.apple.gamed` stderr message causes MSBuild to report failure (exit code -1) even when tests pass, and NUnit results are lost. Use **mlaunch directly** as shown above to capture test output reliably.
198+
199+
> ⚠️ **mlaunch device strings:** Use `xcrun simctl list runtimes` and `xcrun simctl list devicetypes` to find the correct runtime and device type identifiers for your Xcode version. The `--device` format is `:v2:runtime=<runtime-id>,devicetype=<devicetype-id>`.
200+
201+
> ⚠️ **`clean` and `run-bare` must be run from the platform subdirectory** (e.g., `tests/introspection/dotnet/macOS/`), not from the parent `dotnet/` directory. The parent only has `build-%` and `run-%` pattern rules — there are no `clean-%` or `run-bare-%` targets.
153202
154-
> ⚠️ **iOS/tvOS:** Use `make run` (not `make run-bare`) — these require simulator infrastructure that `run-bare` doesn't provide.
203+
> ⚠️ **macOS/MacCatalyst:** Use `run-bare` (not `run`) — `run` launches the app without waiting or capturing stdout. `run-bare` runs the executable directly to capture test output.
155204
156205
Look for this pattern in test output to confirm results:
157206
```
@@ -164,6 +213,8 @@ If introspection tests fail for newly bound types:
164213
- Check if the type crashes on simulator (common for hardware-dependent APIs)
165214
- Add exclusions in the platform-specific `ApiCtorInitTest.cs` files if needed
166215
- Types that crash on init, dispose, or toString need specific exclusion entries
216+
- **NEVER skip an entire namespace** — always add exclusions for specific types only
217+
- **If a `[DesignatedInitializer]` constructor crashes (segfault) when passed null**, the correct fix is to **remove `[NullAllowed]` from that parameter** rather than adding introspection test exclusions. The null is genuinely not allowed by the native API.
167218

168219
If xtro still shows unresolved entries:
169220
- Some APIs may be platform-specific (only available on device, not simulator)

.agents/skills/macios-binding-creator/references/binding-patterns.md

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -269,21 +269,65 @@ For C functions and structs, create manual bindings in `src/FrameworkName/`:
269269
[DllImport (Constants.CoreGraphicsLibrary)]
270270
public static extern void CGContextFillRect (IntPtr context, CGRect rect);
271271

272-
// C Struct — prefer private fields + public properties for easier layout fixes later
272+
// C Struct — use byte backing fields for bools to keep struct blittable
273273
[StructLayout (LayoutKind.Sequential)]
274274
public struct MyStruct {
275-
nfloat x;
276-
nfloat y;
277-
278-
public nfloat X { get => x; set => x = value; }
279-
public nfloat Y { get => y; set => y = value; }
275+
byte enabled;
276+
nfloat x;
277+
nfloat y;
278+
279+
#if !COREBUILD
280+
public bool Enabled {
281+
get => enabled != 0;
282+
set => enabled = value ? (byte) 1 : (byte) 0;
283+
}
284+
285+
public nfloat X { get => x; set => x = value; }
286+
public nfloat Y { get => y; set => y = value; }
287+
#endif
280288
}
281289

282290
// Global constant
283291
[Field ("kMyConstant", "MyFramework")]
284292
public static NSString MyConstant { get; }
285293
```
286294

295+
### Struct Binding Rules
296+
297+
- **Only use blittable types as backing fields in structs.** `bool` and `char` aren't blittable — use `byte` and `ushort`/`short` instead. This avoids `[MarshalAs]` and cecil test known failures.
298+
- **Wrap all public methods and properties in `#if !COREBUILD`** — never use `#pragma warning disable 0169`. Do NOT wrap fields, because bgen may do different things depending on the size of a struct, so it needs to know the final size.
299+
- **NEVER use `XAMCORE_5_0` for new code.** `XAMCORE_5_0` is only for fixing breaking API changes on existing types that shipped in prior releases.
300+
- If a struct member is platform-specific, use `#if !TVOS` (or similar) to exclude it.
301+
302+
### Platform Exclusion for Manual Types
303+
304+
When a manual type (struct, helper class) is not available on tvOS:
305+
306+
```csharp
307+
// In src/FrameworkName/MyStruct.cs:
308+
#if !TVOS
309+
[UnsupportedOSPlatform ("tvos")]
310+
[StructLayout (LayoutKind.Sequential)]
311+
public struct MyStruct {
312+
byte enabled;
313+
314+
#if !COREBUILD
315+
public bool Enabled {
316+
get => enabled != 0;
317+
set => enabled = value ? (byte) 1 : (byte) 0;
318+
}
319+
#endif // !COREBUILD
320+
}
321+
#endif // !TVOS
322+
323+
// In src/frameworkname.cs (at the top of the file):
324+
#if TVOS
325+
using MyStruct = Foundation.NSObject;
326+
#endif
327+
```
328+
329+
The type alias lets tvOS compilation succeed. The `[NoTV]` attribute on the API definition interface ensures the type won't appear in the final tvOS assembly.
330+
287331
## Strongly-Typed Dictionaries
288332

289333
```csharp
@@ -394,12 +438,15 @@ All `[Verify]` attributes must be resolved before submitting a PR.
394438

395439
## Common Pitfalls
396440

397-
- **Null handling**: Always use `[NullAllowed]` where Apple's docs indicate nullability. Default assumption is non-null.
441+
- **Null handling**: Always use `[NullAllowed]` where Apple's docs indicate nullability. Default assumption is non-null. However, if a `[DesignatedInitializer]` constructor crashes (segfault) when passed null, **remove `[NullAllowed]`** — the native API genuinely doesn't accept null, and removing it is better than adding introspection test exclusions.
442+
- **Struct backing fields**: Only use blittable types. `bool` and `char` aren't blittable — use `byte` and `ushort`/`short` instead, with typed property accessors.
398443
- **Threading**: UI APIs require main thread. Use `[ThreadSafe]` for thread-safe APIs.
399-
- **Naming**: Follow .NET PascalCase for methods/properties. Remove redundant ObjC prefixes (`NSString name``string Name`). Acronyms shouldn't be all uppercase (SIMD → Simd, ID → Id when it means "identifier", URL → Url). Methods should be verbs, properties should be nouns.
444+
- **Naming**: Follow .NET PascalCase for methods/properties. Remove redundant ObjC prefixes (`NSString name``string Name`). Acronyms shouldn't be all uppercase (SIMD → Simd, ID → Id when it means "identifier", URL → Url). Methods should be verbs, properties should be nouns. Don't blindly translate ObjC selector names — use .NET-appropriate verb names (e.g., `BuildMenu` not `MenuWithContents`).
400445
- **Selectors**: Must match exactly — a single typo causes runtime crashes.
401446
- **Protocol conformance**: All `[Abstract]` methods in a protocol are required.
402447
- **nint/nuint**: Use `nint`/`nuint` for Objective-C `NSInteger`/`NSUInteger`.
448+
- **XAMCORE_5_0**: Only for fixing breaking changes on existing shipped types. Never use for new code.
449+
- **Struct members**: Wrap public methods and properties in `#if !COREBUILD`, but NOT fields (bgen needs struct size). Never use `#pragma warning disable 0169`.
403450

404451
## Code Style Reminders
405452

.agents/skills/macios-binding-creator/references/test-workflow.md

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,35 @@ rm -rf tests/common/Touch.Unit/Touch.Client/dotnet/obj tests/common/MonoTouch.Di
5454

5555
### Platform-Specific Commands
5656

57-
| Platform | Build | Run | Notes |
58-
|----------|-------|-----|-------|
59-
| iOS | `make -C tests/introspection/dotnet build-ios` | `make run-ios` | Simulator, captures output |
60-
| tvOS | `make -C tests/introspection/dotnet build-tvos` | `make run-tvos` | Simulator, captures output |
61-
| macOS | `make -C tests/introspection/dotnet build-macOS` | `make run-bare-macOS` | Direct execution |
62-
| MacCatalyst | `make -C tests/introspection/dotnet build-MacCatalyst` | `make run-bare-MacCatalyst` | Direct execution |
57+
**Important:** `clean` and `run-bare` must be run from the **platform subdirectory** (e.g., `tests/introspection/dotnet/macOS/`). The parent `dotnet/` directory only has `build-%` and `run-%` pattern rules.
58+
59+
| Platform | Clean | Build | Run |
60+
|----------|-------|-------|-----|
61+
| iOS | `make -C .../dotnet/iOS clean` | `make -C .../dotnet build-ios` | mlaunch directly (see below) |
62+
| tvOS | `make -C .../dotnet/tvOS clean` | `make -C .../dotnet build-tvos` | mlaunch directly (see below) |
63+
| macOS | `make -C .../dotnet/macOS clean` | `make -C .../dotnet/macOS build` | `make -C .../dotnet/macOS run-bare` |
64+
| MacCatalyst | `make -C .../dotnet/MacCatalyst clean` | `make -C .../dotnet/MacCatalyst build` | `make -C .../dotnet/MacCatalyst run-bare` |
65+
66+
### Running iOS/tvOS via mlaunch
67+
68+
`make run-ios`/`run-tvos` uses `dotnet build -t:Run`, which does **NOT reliably capture** the app's stdout. The `com.apple.gamed` stderr message causes MSBuild to report failure (exit code -1) even when mlaunch returns 0, and NUnit test results are lost.
69+
70+
Instead, build first with `make build-ios`/`build-tvos`, then run mlaunch directly:
71+
72+
```bash
73+
# Get the app path
74+
APP_PATH=$(make -C tests/introspection/dotnet/iOS print-executable | sed 's|/introspection$||')
75+
76+
# Run via mlaunch with output capture
77+
SIMCTL_CHILD_NUNIT_AUTOSTART=true \
78+
SIMCTL_CHILD_NUNIT_AUTOEXIT=true \
79+
$DOTNET_DESTDIR/Microsoft.iOS.Sdk/tools/bin/mlaunch \
80+
--launchsim "$APP_PATH" \
81+
--device :v2:runtime=com.apple.CoreSimulator.SimRuntime.iOS-26-4,devicetype=com.apple.CoreSimulator.SimDeviceType.iPhone-16-Pro \
82+
--wait-for-exit:true --
83+
```
84+
85+
Use `xcrun simctl list runtimes` and `xcrun simctl list devicetypes` to find the correct identifiers for your Xcode version.
6386

6487
### Why run-bare for Desktop Platforms
6588

@@ -69,7 +92,11 @@ rm -rf tests/common/Touch.Unit/Touch.Client/dotnet/obj tests/common/MonoTouch.Di
6992

7093
### Why NOT run-bare for Mobile Platforms
7194

72-
iOS and tvOS tests require simulator infrastructure (mlaunch, boot simulator, install app, etc.) that `run-bare` doesn't provide. Always use `make run-ios` / `make run-tvos` for these.
95+
iOS and tvOS tests require simulator infrastructure (boot simulator, install app, etc.) that `run-bare` doesn't provide. Use **mlaunch directly** to launch the app in the simulator with output capture.
96+
97+
**Why not `make run-ios`/`run-tvos`?** These use `dotnet build -t:Run` which wraps mlaunch through MSBuild. The `com.apple.gamed` stderr noise from the simulator causes MSBuild to treat the run as failed (exit code -1), even though mlaunch returns 0 and the tests pass. The NUnit results are also not reliably captured to stdout through the MSBuild layer.
98+
99+
Running mlaunch directly with `SIMCTL_CHILD_NUNIT_AUTOSTART=true` and `SIMCTL_CHILD_NUNIT_AUTOEXIT=true` bypasses MSBuild's error detection and captures the simulator app's stdout (including NUnit results) directly to the terminal.
73100

74101
## Reading Test Results
75102

@@ -97,8 +124,27 @@ Exclusion mechanisms:
97124

98125
### Simulator Infrastructure Errors
99126

100-
`com.apple.gamed` connection errors are a known simulator environment issue. If mlaunch returns exit code 0 and you see the NUnit results pattern, the tests passed despite the error.
127+
`com.apple.gamed` connection errors are a known simulator environment issue. When running via mlaunch directly, these appear as stderr noise but don't affect the test results. When running via `make run-ios`/`run-tvos` (`dotnet build -t:Run`), these stderr messages cause MSBuild to report failure (exit code -1) even though tests pass — this is why running mlaunch directly is preferred.
101128

102129
## Build Timeouts
103130

104131
Builds can take up to 60 minutes. Do not set short timeouts on make/build commands.
132+
133+
## Stale Build Artifacts
134+
135+
If you encounter unexpected failures — types crashing in unrelated frameworks, false "pre-existing" failures, protocol conformance mismatches that shouldn't exist — the most likely cause is **stale `_build/` artifacts**.
136+
137+
**Fix:** Run a full `make all && make install` before re-testing. This rebuilds everything cleanly and installs fresh assemblies.
138+
139+
**Warning signs of stale artifacts:**
140+
- Introspection tests report failures not seen on a clean checkout
141+
- Types crash in `-[description]` or `-[dealloc]` in frameworks you didn't modify
142+
- Cecil tests report unexpected known-failure mismatches
143+
144+
## Introspection Exclusion Rules
145+
146+
When adding exclusions for types that crash on simulator:
147+
148+
- **NEVER skip an entire namespace.** Always add exclusions for specific types only.
149+
- **Prefer fixing the binding over adding test exclusions.** For example, if a `[DesignatedInitializer]` constructor crashes when passed null, remove `[NullAllowed]` from the parameter rather than excluding the type from introspection tests.
150+
- Only add exclusions for genuine simulator/beta SDK bugs that can't be fixed in managed code.

0 commit comments

Comments
 (0)