Skip to content

Add support for Objective-C class implementation #5064

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

harold-b
Copy link
Contributor

@harold-b harold-b commented Apr 21, 2025

Add support for Objective-C class implementation

The purpose of this work is to add full Odin-native support for creating Objective-C classes, not just consuming them.
Doing so without modifying syntax, only leveraging attributes and new compiler intrinsics. I also attempt to leverage existing attributes and augment them when necessarry to add the desired new functionality.

This facilitates creating new classes where needed, for things like delegates, event handlers, etc. without ugly runtime workarounds, or leaving Odin to provide it in a different language.

This PR has the following requirements:

  • Automatically register user-made classes with the Objective-C runtime at startup
  • Allow specifying a superclass
  • Allow association with a single type that would serve as a singular Ivar that is added to the new Objective-C class
  • Allow specifying a selector name manually to be used with the implemented class
  • Provide an easy way to get the ivar value for an Objective-C class.

What it does not aim to do (at least not this PR):

  • Add new syntax
  • Register associated protocols with a class.
  • Force protocol conformance or register protocols during class generation. Supports for things like conformsToProtocol won't be available.
  • Attempt to automatically propagate the Odin state (context) into the Objective-C runtime flow.

Requirements

  • Implement @objc_implement attribute for structs

  • Implement @objc_superclass attribute for procs

    • Disallow superclass cycles
    • Ensure sane ordering during generation so that superclasses get generated first
  • Implement @objc_ivar attribute

  • Limit generating the global ivar offset to intrinsic usage?

  • Implement @objc_selector attribute

  • Apply a legitimate strategy for export/linkage

    • If the @export attribute is applied to an Objective-C class, then all methods are exported. Currently all are exported unconditionally (@export is implied)
  • Checker:

    • Basic preliminary checks
    • Proper checks specification
    • Proper checks applied across all changes
  • Proper, understandable error messages

  • Resolve all pending TODOs

State and discussion

At the time of this writing the full example below works as all the main requirements have been implemented. It will only work with a single module as there's still issues with the changes. Due to lack of experience with the codebase there needs to be some guidance from the team to get this to a proper shippable state.

I'd like to get feedback on attribute and intrinsic naming in addition to the code changes itself.

Practical Explanation & Usage

Creating a new Objective-C class looks like it does when consuming an existing one, except we add the @objc_implement attribute:

@(objc_class="HBAppDelegate", objc_implement)
HBAppDelegate :: struct {
    using _: NSF.Object,
    using _: NS.ApplicationDelegate,
}

Typically you also want to specify a super/base class as well:

@(objc_class="HBAppDelegate", objc_implement, objc_superclass=NS.Object)
HBAppDelegate :: struct { ...

Finally you may want to associate a type to serve as the class' Ivar:

@(objc_class="HBAppDelegate", objc_implement, objc_superclass=NS.Object, objc_ivar=HBAppDelegateT)
HBAppDelegate :: struct {
    using _: NSF.Object,
    using _: NS.ApplicationDelegate,
}

// This serves as the only Ivar for HBAppDelegate, it's just a regular struct. 
HBAppDelegateT :: struct {
    ctx: runtime.Context,
    // ...
}

You can add methods to it the same way you do when consuming existing Objective-C methods, simply add the same @objc_implement attribute:

@(objc_type=HBAppDelegate, objc_implement, objc_name="alloc", objc_is_class_method=true)
HBAppDelegate_alloc :: proc "c" () -> ^HBAppDelegate {
    return msgSend(^HBAppDelegate, HBAppDelegate, "alloc")
}

The compiler will generate a hidden function that conforms to the signature that the Objective-C runtime expects, which simply forwards the call to our method proc:

__$objc_method::MyFoo::alloc :: proc "c" ( self: objc_id, _cmd: objc_SEL ) -> ^HBAppDelegate {
    return HBAppDelegate_alloc()
}

Any arguments will be forwarded:

@(objc_type=HBAppDelegate, objc_implement, objc_name="initWithContext")
HBAppDelegate_initWithContext :: proc "c" ( self: ^HBAppDelegate, ctx: runtime.Context ) -> ^HBAppDelegate {
    s := intrinsics.ivar_get(self, HBAppDelegateT)
    s.ctx = ctx

    return self
}

// Generates:
__$objc_method::MyFoo::alloc :: proc "c" ( self: objc_id, _cmd: objc_SEL, ctx: runtime.Context ) -> ^HBAppDelegate {
    return HBAppDelegate_initWithContext(self, ctx)
}

Notice the use of the new intrinsic intrinsics.ivar_get. This intrinsic will obtain a pointer to the Type's data of the Ivar from an instance, if one is associated with the class. This is implemented by recording the offset to the ivar at startup, then it simply offsets from the instance pointer during the call to the intrinsic.

You can also specify the selector name to be used for a method. By default the value of @objc_name will be used, if one is not specified. But there will be many cases when you may want to specify one manually, as it is when implementing a protocol. Specify it via the @objc_selector attribute:

@(objc_type=HBAppDelegate, objc_implement, objc_name="applicationWillFinishLaunching", objc_selector="applicationWillFinishLaunching:")
HBAppDelegate_applicationWillFinishLaunching :: proc "c" (self: ^HBAppDelegate, notification: ^NS.Notification) {
...
}

@(objc_type=HBAppDelegate, objc_implement, objc_name="applicationDidFinishLaunching", objc_selector="applicationDidFinishLaunching:")
HBAppDelegate_applicationDidFinishLaunching :: proc "c" (self: ^HBAppDelegate, notification: ^NS.Notification) {
...
}

Full working example of an NSAppDelegate (partially) implemented in Odin:

package objcsandbox

import "base:intrinsics"
import "base:runtime"
import "core:log"
import NSF "libs:darwodin/Foundation"
import NS  "libs:darwodin/AppKit"

msgSend  :: intrinsics.objc_send
ivar_get :: intrinsics.ivar_get


main :: proc() {
    context.logger = log.create_console_logger()

    app := NS.Application.sharedApplication()
    app->setActivationPolicy(.Regular)
    app->activate()
    app->setDelegate(HBAppDelegate.alloc()->initWithContext(context))
    app->run()
}

State :: enum {
    Initializing,
    Launching,
    FinishedLaunching,
}
HBAppDelegateT :: struct {
    ctx:   runtime.Context,
    state: State,
}

@(objc_class="HBAppDelegate", objc_superclass=NSF.Object, objc_implement, objc_ivar=HBAppDelegateT)
HBAppDelegate :: struct {
    using _: NSF.Object,
    using _: NS.ApplicationDelegate,
}

@private
get_self :: #force_inline proc "c" ( self: ^HBAppDelegate ) -> ^HBAppDelegateT {
    return ivar_get(self, HBAppDelegateT)
}

@(objc_type=HBAppDelegate, objc_implement, objc_name="alloc", objc_is_class_method=true)
HBAppDelegate_alloc :: proc "c" () -> ^HBAppDelegate {
    return msgSend(^HBAppDelegate, HBAppDelegate, "alloc")
}

@(objc_type=HBAppDelegate, objc_implement, objc_name="initWithContext")
HBAppDelegate_initWithContext :: proc "c" ( self: ^HBAppDelegate, ctx: runtime.Context ) -> ^HBAppDelegate {
    s := get_self(self)
    s.ctx = ctx

    return self
}

@(objc_type=HBAppDelegate, objc_implement, objc_name="applicationWillFinishLaunching", objc_selector="applicationWillFinishLaunching:")
HBAppDelegate_applicationWillFinishLaunching :: proc "c" (self: ^HBAppDelegate, notification: ^NSF.Notification) {
    s := get_self(self)
    context = s.ctx
    log.infof("applicationWillFinishLaunching: %v -> %v", s.state, State.Launching)
    s.state = .Launching
}

@(objc_type=HBAppDelegate, objc_implement, objc_name="applicationDidFinishLaunching", objc_selector="applicationDidFinishLaunching:")
HBAppDelegate_applicationDidFinishLaunching :: proc "c" (self: ^HBAppDelegate, notification: ^NSF.Notification) {
    s := get_self(self)
    context = s.ctx
    log.infof("applicationDidFinishLaunching: %v -> %v", s.state, State.FinishedLaunching)
    s.state = .FinishedLaunching
}

@harold-b harold-b changed the title Add initial support for Objective-C class implementation Add support for Objective-C class implementation Apr 21, 2025
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