Skip to content

Add opt-in Objective-C binding generation#780

Merged
tannergooding merged 5 commits into
dotnet:mainfrom
tannergooding:tannergooding-triage-398-254-transpile-objc
Jul 15, 2026
Merged

Add opt-in Objective-C binding generation#780
tannergooding merged 5 commits into
dotnet:mainfrom
tannergooding:tannergooding-triage-398-254-transpile-objc

Conversation

@tannergooding

Copy link
Copy Markdown
Member

Opt-in Objective-C binding generation, behind --generate-objective-c-bindings. This is the first phased slice of #254 and mirrors how we already emit COM: expose the raw C ABI and a friendly wrapper on the same type, keeping the layout faithful.

The parse/interop half of #254 has been in place for years (ClangSharp self-hosts the full Clang AST, and clangsharp_Cursor_getSelector already exists), so this PR is emitter-only.


Model

An @protocol/@interface/category maps to a public unsafe partial struct whose only field is void* isa — a byte-for-byte struct objc_object. The object pointer is this; isa is the identity/class hook. Unlike COM's lpVtbl there is no fixed dispatch table, because Objective-C dispatch is dynamic through objc_msgSend. Modern non-fragile ivars are exactly why the struct is opaque rather than a fixed-layout record.

Each in-subset method/property emits: a cached SEL in a nested public static partial class Selectors, and a friendly member that casts the single raw objc_msgSend entry point to the call site's exact delegate* unmanaged<...> signature and invokes it with the implicit (self, _cmd) leading args. Instance members recover self via Unsafe.AsPointer(ref this); class (+) members are static and dispatch on objc_getClass(...). A small inline ObjectiveC/SEL runtime surface is emitted once per output.

ABI

Targets arm64 only, and says so. On Apple Silicon there is no objc_msgSend_stret/_fpret split — struct and FP returns/args all go through the single objc_msgSend under AAPCS64, which is what the JIT follows for the generated signature. By-value aggregates are therefore emitted through plain objc_msgSend with the real struct type, each flagged with a one-line note that x86-64 _stret is intentionally unsupported (Apple dropped x86-64 macOS). This sidesteps per-triple variant selection entirely.

What's included

  • Opt-in flag + decl dispatch for ObjCProtocol, ObjCInterface, ObjCCategory (goto default when off).
  • @protocol → opaque struct + isa + Selectors + friendly instance methods/properties.
  • @interface → same, plus the backing Class object and class (+) members.
  • @required/@optional surfaced, with a respondsToSelector:-guard note on optional members.
  • Inheritance + adopted protocols recorded in [NativeTypeName]; members are not flattened (dynamic dispatch + identical layout means a pointer cast to the superclass just works).
  • ObjCObjectPointerType mapping (idvoid*, NSObject *NSObject*).
  • By-value aggregate return/param support (arm64, flagged).
  • Categories reopen the class's partial struct and partial Selectors, adding only new members.
  • Out-of-subset constructs are diagnosed and skipped rather than mis-emitted: variadic methods, and class members on a @protocol (no backing class).
  • 8 golden tests (Windows + Unix variants where relevant); full suite green, 0 warnings.

Non-goals (intentional)

  • Variadic methods — a C ... can't be a fixed function-pointer signature.
  • @implementation — definition-side; adds no callable surface over the @interface.
  • Foundation/SDK base types — users point the generator at the SDK headers, same as any other platform SDK.
  • x86-64 macOS — dropped by Apple; the _stret/_fpret variants are not supported.
  • ARC / retain-release — no C# equivalent; ownership is the caller's, as with any unsafe interop.

Before this is ready to merge

  • API review for the new config option and emitted public surface.
  • A decision on whether the inline ObjectiveC/SEL runtime surface should stay generated, move to a small hand-written support library, or be left to the consumer.
  • On-device arm64 validation — the golden tests prove shape, not runtime correctness.

Contributes to #254.

tannergooding and others added 5 commits July 14, 2026 19:15
Introduce `--generate-objective-c-bindings`, mapping an `@protocol` onto a
layout-faithful struct (first field `isa`, mirroring `struct objc_object`) with a
raw `Selectors` cache and friendly `objc_msgSend` members, plus a small inline
`libobjc` support surface (`SEL`, `objc_msgSend`, `sel_registerName`, `objc_getClass`).

Modeled on the COM feature: the struct is the object and is used via `Greeter*`,
with the raw selector/msgSend substrate and friendly members exposed simultaneously.
Scoped to instance methods with scalar/pointer/void signatures; variadics and
by-value aggregates are diagnosed rather than emitted with a wrong ABI. Off by
default, so existing behavior is unchanged.

Also fix a NullReferenceException in `VisitRef` when a stray top-level Objective-C
ref hangs off the translation unit and there is no active output builder.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Map instance @Property declarations onto friendly members: readonly maps to
an expression-bodied getter, read-write to a get/set block dispatching the
synthesized setter selector. Selectors are deduplicated across methods and
accessors, and a shared BuildObjCMsgSend helper emits the per-call-site cast.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Generalize the emitter to any ObjCContainerDecl and map @interface onto the
same opaque { void* isa } struct as @protocol (modern Objective-C uses
non-fragile ivars, so there is no stable field layout to mirror), plus a cached
Class object. Class (+) methods and properties become static members dispatched
on Class; instance members dispatch on this. Record the superclass and adopted
protocols in NativeTypeName (and NativeInheritance when enabled) rather than
flattening, since dispatch is dynamic and every struct shares one layout.

Flag @optional protocol members so callers know to guard with
respondsToSelector:, and map ObjCObjectPointerType (id, NSObject *) to the
generated struct pointer or void* instead of falling back to the native
spelling.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Emit aggregate return/parameter types through the plain objc_msgSend entry
point, which is ABI-correct on arm64 where there is no objc_msgSend_stret. Flag
each affected member with a note that x86-64 stret is unsupported. Only variadic
methods remain outside the message-send subset.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Map `@interface Foo (Bar)` onto a reopened `partial struct` for the class,
emitting only the category's new message members. Make the nested `Selectors`
cache `partial` so the interface and each category can contribute selectors;
class members continue to dispatch on the interface's `Class` object.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@tannergooding
tannergooding marked this pull request as ready for review July 15, 2026 02:21
@tannergooding
tannergooding merged commit 83b31be into dotnet:main Jul 15, 2026
12 checks passed
@tannergooding
tannergooding deleted the tannergooding-triage-398-254-transpile-objc branch July 15, 2026 02:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant