Create C# bindings for Apple frameworks in dotnet/macios. USE FOR: binding new APIs, implementing .todo file entries, creating Xcode SDK bindings, binding AVFoundation/UIKit/AppKit or any Apple framework, "bind this framework", "implement these APIs". DO NOT USE FOR: Xcode beta version bumps (use macios-xcode-beta-update skill), CI failure investigation (use macios-ci-failure-inspector skill).
77
96%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Create C# bindings for Apple platform APIs in the dotnet/macios repository. This skill encodes the end-to-end workflow: from reading .todo files through implementation, building, and validating with xtro, cecil, and introspection tests on all platforms.
Use this skill when:
.todo files in tests/xtro-sharpie/api-annotations-dotnet/./configure already run)XCODE_DEVELOPER_ROOT pathmake world or make all && make install already completedCheck the .todo files to see what APIs are missing:
ls tests/xtro-sharpie/api-annotations-dotnet/*-{FrameworkName}.todo
cat tests/xtro-sharpie/api-annotations-dotnet/iOS-{FrameworkName}.todoEach .todo file lists missing APIs per platform (iOS, tvOS, macOS, MacCatalyst). The format is:
!missing-selector! ClassName::methodName: not bound
!missing-type! ClassName not bound
!missing-field! ClassName FieldName not bound
!missing-enum-value! EnumName::ValueName not bound❌ NEVER bind APIs that aren't in the
.todofiles unless explicitly asked. The.todofiles are the source of truth for what's missing.
Run the xtro generator to produce reference C# bindings from the SDK headers:
make -C tests/xtro-sharpie gen-allThis creates generated .cs files you can search to find the correct C# signatures, attributes, and patterns for the APIs you need to bind. Use these as reference — don't copy them verbatim.
Before implementing, understand the native API:
$XCODE_DEVELOPER_ROOT)src/frameworkname.cs for patterns used in the same frameworkBefore writing any bindings, determine the correct availability version for each API. The version represents when Apple introduced the API, not the current SDK version.
Primary source of truth: the generated reference bindings from Step 2. After running make -C tests/xtro-sharpie gen-all, search the generated .cs files for the API you're binding — they include [Introduced] attributes extracted from Apple's SDK headers with the correct per-platform introduction versions. Always use these versions.
# Find the generated reference binding for a specific API.
# gen-all (Step 2) writes these to api/<Platform>/ApiDefinition.cs (gitignored),
# so run Step 2 first. Widen to api/*/ if a symbol isn't in ApiDefinition.cs.
grep -rn "SomeClassName\|SomeMethodName" tests/xtro-sharpie/api/*/ApiDefinition.csIf the generated reference bindings don't include version information, fall back to these sources:
$XCODE_DEVELOPER_ROOT for API_AVAILABLE macrosSdkVersions.cs — use this only for brand-new APIs introduced in the current Xcode release:grep -E 'public const string (iOS|TVOS|OSX|MacCatalyst) ' tools/common/SdkVersions.cs❌ NEVER assume the current SDK version is the introduction version for all APIs. The SDK version (e.g.,
26.5) is only correct for APIs that are genuinely new in this Xcode release. When adding an existing framework to a new platform (e.g., MediaSetup to MacCatalyst), or adding enum members that were introduced in an earlier release, the introduction version will be different — check the generated reference bindings or Apple headers.
If the user specifies a version, use that instead. Ask the user if you're unsure which version to use.
Bindings go in these locations:
src/frameworkname.cs — API definitions (interfaces with [Export] attributes)src/FrameworkName/ — Manual code (partial classes, enums, P/Invokes, extensions)src/frameworks.sources — Maps frameworks to source files (update if adding new files)⚠️ Binding an entirely new framework (no
src/<fw>.csyet) needs extra build/test wiring —frameworks.sources,tools/common/Frameworks.cs,ProjectTest.cslink lists, the xtro ignore lists, plus two generated-file build gotchas. See references/binding-patterns.md § "Registering a Brand-New Framework".
Key binding patterns:
// New property on existing class
[Export ("allowsCaptureOfClearKeyVideo")]
bool AllowsCaptureOfClearKeyVideo { get; set; }
// New method on existing class
[Export ("setCaptionPreviewProfileId:")]
void SetCaptionPreviewProfileId ([NullAllowed] string profileId);
// New notification field
[Field ("AVPlayerInterstitialEventMonitorScheduleRequestedNotification")]
[Notification]
NSString ScheduleRequestedNotification { get; }❌ NEVER forget platform availability attributes. Every new API must have
[iOS],[Mac],[TV],[MacCatalyst], and/or[No*]attributes matching the.todofile platforms where the API appears. This includes all binding types:
- API definition interfaces and members in
src/frameworkname.cs— use[iOS (X, Y)],[Mac (X, Y)], etc.- P/Invoke wrappers and manual properties in
src/FrameworkName/*.cs— use[SupportedOSPlatform ("iosX.Y")],[SupportedOSPlatform ("macos")], etc.- Fields, constants, and enum values
- Individual enum members added to an existing enum — match the native header per member: add
[iOS (X, Y)]/[No*]/etc. only for what the header annotates on that member (its ownAPI_AVAILABLEversion newer than the enum, orAPI_UNAVAILABLE/absence on a platform). A member the header doesn't annotate inherits the enum's availability — add no attribute, matching its siblings. On a multi-platform enum, if you do add a per-member introduced version, add it for every applicable platform (bgen otherwise back-fills the enum's older version). Error enums are an exception — see the next rule. See references/binding-patterns.md § "Adding New Members to Existing Enums".
❌ NEVER put availability or unavailability attributes on members of an error enum — not even brand-new ones. An error enum is any enum whose name ends in
ErrororErrorCode, or that carries[ErrorDomain]. The cecil testEnumTest.NoAvailabilityOnError(issue #9724) fails — with no known-failures allowlist — if any field carries an availability/unavailability attribute ([iOS]/[Mac]/[TV]/[No*]/[Introduced]/[Unavailable]/[Supported/UnsupportedOSPlatform]); only[Obsolete]/[Obsoleted…]deprecation attributes are exempt. Bind new error-code members bare — just the value (plus[Field]if smart) — matching their siblings (e.g.VNErrorCode.ResourceUnavailable,UNErrorCode.AttachmentUnsupportedType).
❌ NEVER use
string.Empty— use"". Never useArray.Empty<T>()— use[].
❌ NEVER add placeholder XML documentation text like
"To be added."anywhere — not in<remarks>,<summary>,<returns>,[Async (XmlDocs = ...)], or any other XML doc element. Either write meaningful documentation or omit the element entirely.
❌ NEVER forget
[NullAllowed]onout NSError errorparameters. Every method that takesNSError**(bound asout NSError error) must use[NullAllowed] out NSError error. This applies to all error-returning methods — the error output is null on success.
❌ NEVER forget
#nullable enableat the top of every new C# file you create.
⚠️ PREFER a named delegate type (
delegate void SomeFrameworkSomeCallback (…)) overAction<T>/Func<T>for completion-handler / callback parameters when the meaning of a parameter isn't obvious from its type — only a named delegate can carry parameter names and XML docs. (Action<T>/Func<T>now support nullable type arguments, so[NullAllowed]/nullability is no longer a reason to avoid them.) See references/binding-patterns.md § "Blocks and Completion Handlers".
❌ NEVER use non-blittable types (
bool,char) as backing fields in structs. Usebyte(forbool) andushort/short(forchar) with property accessors. See references/binding-patterns.md for the correct pattern.
❌ NEVER use
XAMCORE_5_0for new code.XAMCORE_5_0is only for fixing breaking API changes on existing types that shipped in prior releases. However, when xtro reports a mismatch on an existing type (e.g., wrong enum backing type, missing[Native]), and fixing it directly would be a breaking change, you must use#if XAMCORE_5_0guards to preserve binary compatibility while queuing the fix for the future. Add a.ignoreentry for the xtro mismatch. See references/binding-patterns.md § "XAMCORE_5_0 Pattern for Existing Types".
❌ NEVER use
#pragma warning disable 0169for struct fields. Instead, wrap public methods and properties inside#if !COREBUILD(but NOT fields — bgen needs to know the struct size).
⚠️ Protocol methods returning opaque types: If a protocol method returns an opaque C type (e.g.,
PMPrintSession) that has a managedNativeObjectwrapper insrc/FrameworkName/, do NOT useIntPtr. Register the type as a bgen marshal type so bgen can generate properRuntime.GetINativeObject<T>()marshaling. See references/binding-patterns.md § "NativeObject Return Types in Protocol Methods".
⚠️ Place a space before parentheses and brackets:
Foo (),Bar (1, 2),myarray [0].
⚠️ Method names should follow .NET naming conventions — use verb-based names, not direct Objective-C selector translations (e.g.,
BuildMenunotMenuWithContents).
❌ NEVER change the casing of the Objective-C class name prefix in C# type names.
ARSessionstaysARSession(notArSession),AVPlayerstaysAVPlayer(notAvPlayer). But an acronym inside the name (after the prefix) DOES follow .NET rules —NSURLSession→NSUrlSession,NSURLSessionHandler→NSUrlSessionHandler(theNSprefix is kept, butURLbecomesUrl). When creating new manual types, match the framework's established prefix (e.g., all ARKit types useAR*, all CoreGraphics types useCG*). The .NET acronym rules (SIMD → Simd, URL → Url) apply within property/method names and to acronyms inside type names, NOT to the leading class prefix. (A few frameworks instead preserve an inner acronym across their whole family — e.g. CoreGraphicsCGPDF*— so match the existing sibling types when a framework is consistent.)
⚠️ For in depth binding patterns and conventions See references/binding-patterns.md
⚠️ Struct array parameters: When an API takes a C struct pointer + count (e.g.,
MyStruct*+NSUInteger), bind the raw pointer as[Internal]withIntPtr, then create a manual public wrapper using the factory pattern withfixed. See references/binding-patterns.md § "Struct Array Parameter Binding".
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:
src/FrameworkName/MyStruct.cs), wrap the struct body with #if !__TVOS__[UnsupportedOSPlatform ("tvos")] on the structsrc/frameworkname.cs), add a type alias at the top so compilation succeeds:#if __TVOS__
using MyStruct = Foundation.NSObject;
#endifThe [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.
❌ NEVER use platform-specific source file lists (e.g., appending a per-framework list to
MACOS_DOTNET_SOURCES) for platform-conditional code. Instead, use preprocessor directives (#if __MACOS__,#if !__TVOS__,#if __IOS__) within shared source files. Platform-specific source file lists are for the build system, not for conditional compilation of individual types or members.
Available preprocessor symbols for platform checks:
__MACOS__ (preferred) / MONOMAC — macOS__IOS__ — iOS__TVOS__ (preferred) / TVOS — tvOS__MACCATALYST__ — Mac Catalyst⚠️ Mac Catalyst defines both
__MACCATALYST__and__IOS__(msbuild/Xamarin.Shared/Xamarin.Shared.props). In#ifchains, test__MACCATALYST__before__IOS__, or the Catalyst case falls into the iOS branch.
⚠️ Foundation/TextKit types shared by AppKit and UIKit (e.g.
NSTextList,NSParagraphStyle) are bound once insrc/xkit.cs, not duplicated inappkit.cs/uikit.cs. If a type inappkit.cs(oruikit.cs) becomes exposed to the other side, consolidate it there (share identical enums, split only divergent ones, handle platform-only members with[No*]attributes — reserving#iffor divergences attributes can't express — keep back-dated availability). See references/binding-patterns.md → "Shared AppKit/UIKit Types".
Rebuild and install so the test suites — which read the installed NuGet packs, not src/build/ — pick up your changes:
make all && make install❌ NEVER use
make -C src build. There is nobuildtarget insrc/Makefile, so the command is unreliable: on a fresh checkout it fails withmake: *** No rule to make target 'build', and once thesrc/build/output directory exists the wordbuildmatches that directory and the command silently no-ops (Nothing to be done for 'build'). Either way it compiles nothing and you validate against stale (or missing) assemblies. Usemake all && make install(ormake worldfor a full rebuild).
Fix any compilation errors before proceeding. Builds can take up to 60 minutes — do not timeout early.
For any manually bound APIs (P/Invokes, manual properties on partial classes, struct accessors), add tests in tests/monotouch-test/{FrameworkName}/.
⚠️ Only run monotouch-tests (Step 6d) if you added or modified test files in this step. If no manual bindings were added (i.e., all APIs were bound via
[Export]in the API definition file), skip both this step and Step 6d.
using CoreText; // framework being tested
using NUnit.Framework;
namespace MonoTouchFixtures.CoreText { // MonoTouchFixtures.{FrameworkName}
[TestFixture]
[Preserve (AllMembers = true)]
public class FontTest {
[Test]
public void UIFontType_SystemFont ()
{
TestRuntime.AssertXcodeVersion (26, 4); // match the availability version
using (var font = new CTFont ("Helvetica", 12)) {
var fontType = font.UIFontType;
Assert.AreEqual (CTUIFontType.System, fontType);
}
}
}
}Key patterns:
MonoTouchFixtures.{FrameworkName} (e.g., MonoTouchFixtures.CoreText)TestRuntime.AssertXcodeVersion (major, minor) matching the API's availability version. This skips the test on older runtimes instead of failing.using statements for handle-based types⚠️ If adding a new test file, make sure the
.csprojattests/monotouch-test/picks it up (it typically uses wildcard includes, but verify).
See references/binding-patterns.md for more monotouch-test patterns.
⚠️ Stale build artifacts: If you encounter unexpected test failures (SIGABRT, segfaults in unrelated types, false "pre-existing" failures), always run
make worldFIRST before investigating. Never conclude a failure is "pre-existing" without rebuilding — stale_build/artifacts are the #1 cause of spurious introspection crashes after binding changes.
Run all three test suites. Run them sequentially, not in parallel.
There are no run-ios/run-tvos/run-macos/run-maccatalyst xtro targets. Regenerate the reference bindings, then classify every platform (this also runs the sanity check):
make -C tests/xtro-sharpie gen-all
make -C tests/xtro-sharpie dotnet-classifydotnet-classify classifies all platforms and then runs sanity. When a .todo entry has been resolved by your binding but the .todo file still lists it, sanity prints ?fixed-todo? and exits non-zero — that is the cleanup signal, not a passing result. Loop until it passes:
?fixed-todo? entry you bound, remove that line from its .todo file (and git rm the file if it becomes empty — see next note).make -C tests/xtro-sharpie dotnet-classify until it prints Sanity check passed (exit 0).💡 Setting
AUTO_SANITIZE=1(e.g.AUTO_SANITIZE=1 make -C tests/xtro-sharpie dotnet-classify) makes xtro auto-remove the resolved?fixed-todo?lines and delete emptied.todo/.ignorefiles for you. Any surrounding comments related to those entries still have to be removed manually.
Any entries that remain unresolved need binding or explicit .ignore entries with justification.
⚠️
!extra-enum-value!: if classify reports a managed enum value that the native header marks unavailable on a platform, fix it at the right scope — put[No<Platform>]on the whole enum type only if the entire native enum is unavailable there, otherwise put[No<Platform>]on the individual value(s). Never mark the whole type just to silence one value (it strips valid members likeNone). See references/test-workflow.md § "!extra-enum-value!".
❌ ALWAYS delete empty
.todofiles after resolving all entries:git rm tests/xtro-sharpie/api-annotations-dotnet/{platform}-{Framework}.todo. Do not leave empty.todofiles in the repository — they cause xtro test noise.
make -C tests/cecil-tests run-tests⚠️ Adding public members can fail
VerifyEveryVisibleMemberIsDocumented— the failure lists your new, undocumented members. Either write real XML documentation for them, or — if the framework's existing members are already listed intests/cecil-tests/Documentation.KnownFailures.txt(the whole framework is undocumented) — regenerate that baseline to stay consistent:WRITE_KNOWN_FAILURES=1 make -C tests/cecil-tests run-tests(this run exits non-zero by design), then re-run without the env var to confirm exit 0. Verify thegit diffof the known-failures file contains only your new members. See references/test-workflow.md.
IMPORTANT: Clean shared obj directories before each platform to avoid NETSDK1005 errors:
# iOS — build, then run via mlaunch directly for reliable output capture
rm -rf tests/common/Touch.Unit/Touch.Client/dotnet/obj tests/common/MonoTouch.Dialog/obj
make -C tests/introspection/dotnet/iOS clean
make -C tests/introspection/dotnet build-ios
# Get the app path and run via mlaunch directly:
APP_PATH=$(make -C tests/introspection/dotnet/iOS print-executable | sed 's|/introspection$||')
SIMCTL_CHILD_NUNIT_AUTOSTART=true \
SIMCTL_CHILD_NUNIT_AUTOEXIT=true \
$DOTNET_DESTDIR/Microsoft.iOS.Sdk/tools/bin/mlaunch \
--launchsim "$APP_PATH" \
--device :v2:runtime=com.apple.CoreSimulator.SimRuntime.iOS-26-4,devicetype=com.apple.CoreSimulator.SimDeviceType.iPhone-16-Pro \
--wait-for-exit:true --
# tvOS — same approach as iOS
rm -rf tests/common/Touch.Unit/Touch.Client/dotnet/obj tests/common/MonoTouch.Dialog/obj
make -C tests/introspection/dotnet/tvOS clean
make -C tests/introspection/dotnet build-tvos
APP_PATH=$(make -C tests/introspection/dotnet/tvOS print-executable | sed 's|/introspection$||')
SIMCTL_CHILD_NUNIT_AUTOSTART=true \
SIMCTL_CHILD_NUNIT_AUTOEXIT=true \
$DOTNET_DESTDIR/Microsoft.tvOS.Sdk/tools/bin/mlaunch \
--launchsim "$APP_PATH" \
--device :v2:runtime=com.apple.CoreSimulator.SimRuntime.tvOS-26-4,devicetype=com.apple.CoreSimulator.SimDeviceType.Apple-TV-4K-3rd-generation-4K \
--wait-for-exit:true --
# macOS (use run-bare for direct execution with captured output)
rm -rf tests/common/Touch.Unit/Touch.Client/dotnet/obj tests/common/MonoTouch.Dialog/obj
make -C tests/introspection/dotnet/macOS clean build
make -C tests/introspection/dotnet/macOS run-bare
# MacCatalyst (use run-bare for direct execution with captured output)
rm -rf tests/common/Touch.Unit/Touch.Client/dotnet/obj tests/common/MonoTouch.Dialog/obj
make -C tests/introspection/dotnet/MacCatalyst clean build
make -C tests/introspection/dotnet/MacCatalyst run-bare⚠️ iOS/tvOS output capture:
make run-ios/run-tvosusesdotnet build -t:Runwhich does NOT reliably capture the app's stdout. Thecom.apple.gamedstderr 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.
⚠️ mlaunch device strings: Use
xcrun simctl list runtimesandxcrun simctl list devicetypesto find the correct runtime and device type identifiers for your Xcode version. The--deviceformat is:v2:runtime=<runtime-id>,devicetype=<devicetype-id>.
⚠️
cleanandrun-baremust be run from the platform subdirectory (e.g.,tests/introspection/dotnet/macOS/), not from the parentdotnet/directory. The parent only hasbuild-%andrun-%pattern rules — there are noclean-%orrun-bare-%targets.
⚠️ macOS/MacCatalyst: Use
run-bare(notrun) —runlaunches the app without waiting or capturing stdout.run-bareruns the executable directly to capture test output.
⚠️ Host-OS version gating (brand-new-SDK APIs): introspection gates every check to the running OS (
PlatformInfo.Host.Version). On a host whose macOS is older than the SDK you bound (e.g. binding 27.0 APIs on a macOS 26 host), the macOS/MacCatalystrun-bareruns can't exercise the new symbols — they're gated away, not crashed — introspection'sSkipDueToAttributeskips any member not available on the host OS (IsAvailableOnHostPlatform), so the check silently doesn't run and a clean pass there does not validate them. Validate instead on an iOS/tvOS simulator whose runtime matches the new SDK — bump the--device runtime=…in the commands above to the new-SDK runtime (e.g.iOS-27-0instead ofiOS-26-4), whereApiFieldTest/ApiSelectorTestactually resolve the new symbols. (For APIs available only on macOS/Mac Catalyst there's no simulator fallback — validate on a host running the matching or newer macOS.)
Look for this pattern in test output to confirm results:
Tests run: X Passed: X Inconclusive: X Failed: X Ignored: X⚠️ Beta-SDK protocol-conformance failures (e.g.
X conforms to NSSecureCoding but does not implement INSSecureCoding) usually mean the runtime conformsXto a protocol the header doesn't declare. If xtro is silent (no!missing-protocol-conformance!— confirm the header truly lacks it, else fix the binding), tolerate it with a test-only introspection Skip inApiProtocolTest.cs(orMacApiProtocolTest.cs/iOSApiProtocolTest.cs) under the matchingcase "<Protocol>":— not by adding the conformance to the binding. See references/test-workflow.md → "Runtime-Only Protocol Conformance".
Skip this step if no monotouch-test files were added or modified.
Run per-platform, using exact casing for platform names:
# iOS
make -C tests/monotouch-test/dotnet/iOS run
# tvOS
make -C tests/monotouch-test/dotnet/tvOS run
# macOS (use run-bare for captured output)
make -C tests/monotouch-test/dotnet/macOS run-bare
# MacCatalyst (use run-bare for captured output)
make -C tests/monotouch-test/dotnet/MacCatalyst run-bare⚠️ Platform casing matters: Use
iOS,tvOS,macOS,MacCatalystexactly — notios,macos, etc.
⚠️ Desktop platforms: Use
run-bare(notrun) for macOS and MacCatalyst — same reason as introspection:runlaunches without capturing stdout. Therun-baretarget exists for every platform (it just runs the built executable directly), but it doesn't work for iOS/tvOS — those need the simulator viarun/mlaunch.
If introspection tests fail for newly bound types:
ApiCtorInitTest.cs files if needed[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.DesignatedInitializer test reports <Type> should re-expose <Base>::.ctor(...) — you bound a subclass (e.g. of AUAudioUnit) that inherits a designated initializer but doesn't re-declare it. The subclass must re-expose it. Simplest fix that passes with no other changes: re-declare the init as a public [DesignatedInitializer] Constructor with the same selector/signature. See references/binding-patterns.md § "Re-exposing Designated Initializers in Subclasses" — including the failable-initializer (factory) variant, which additionally requires a Match () case in ApiCtorInitTest.cs.not found / does not respond for an API you just bound (common on a beta OS), and the SDK header does declare that selector for this platform — it's a beta-runtime gap, not a binding bug: the binding is correct but the beta OS hasn't implemented the selector yet. Do not change availability or add [No<Platform>] (that would make xtro report the API missing). Add a narrow skip in the selector test (ApiSelectorTest, not ApiCtorInitTest) — MacApiSelectorTest.cs (macOS) or iOSApiSelectorTest.cs (iOS/tvOS/MacCatalyst) — for only the failing platform(s), unconditional on real hardware (macOS/MacCatalyst) and TestRuntime.IsSimulator-gated only for simulator-only gaps. See references/test-workflow.md § "Selector Not Found (Declared but Not Implemented)".If xtro still shows unresolved entries:
.ignore entries with comments explaining why they can't be bound.todo entries for known limitations.todo files unless explicitly asked.When reporting results, use this structure:
.todo entries intentionally left unbound, with reasons354c2a6
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.