D/Objective-C Preliminary Design

Recently, I started working on integrating support for the Objective-C object model inside the D compiler (see this post). In a certain way, it’s more like I’m porting the D programming language to the Objective-C runtime. I’m giving D semantics to Objective-C objects, and if all goes well this could bring many features of D to Cocoa programmers willing to switch to D. Since I’ve been working on this for a few months now, I thought it’d be a good idea to take a break and write down how I expect things to look like once completed.

So here is a draft document that outlines how D with Objective-C support will work. Some parts are already implemented, most of it remains to do however. Please comment.


Objective-C and D

On some platforms, D can interact directly with Objective-C objects almost exactly as if they were regular D objects. All we need to do is provide a declaration for the classes we want to use.

When interacting with Objective-C objects or creating new derived classes, the D compiler will emit compiled code similar to what an Objective-C compiler would emit. There is no performance penalty when calling Objective-C code from D.

Using an existing Objective-C class

To use an existing Objective-C class, we must first write a declaration for that class, and we must mark this class as comming from Objective-C. Here is an abbreviated declaration for class NSComboBox:

extern (Objective-C)
class NSComboBox : NSTextField
{
    private ObjcObject _dataSource;
    ...
}

This declaration will not emit any code because it was tagged as extern (Objective-C), but it will let know to the compiler that the NSComboBox class exists and can be used. Since NSComboBox derives from NSObject, the NSObject declaration must also be reacheable or we’ll get an error.

Declaring members variables of the class is important. Even if we don’t plan on using them, they are needed to properly calculate the size of derived classes.

Declaring Instance Methods

Objective-C uses a syntax that greatly differs from D when it comes to calling member functions — instance methods and class methods in Objective-C parlance. In Objective-C, a method is called using the following syntax:

[comboBox insertItemWithObjectValue:val atIndex:idx];

This will call the method insertItemWithObjectValue:atIndex: on the object comboBox with two arguments: val and idx.

To make Objective-C methods accessible to D programs, we need to map them to a D function name. This is acomplished by declaring a member function and giving it a selector:

extern (Objective-C)
class NSComboBox : NSTextField
{
    private void* _dataSource;

    void insertItem(ObjcObject object, NSInteger value) [insertItemWithObjectValue:atIndex:];
}

Now we can call the method in our D program as if it was a regular member function:

comboBox.insertItem(val, idx);

Overloading

Objective-C does not support function overloading, which makes it impossible to have two methods with the same name. D supports overloading, and we can take advantage of that in a class declaration:

extern (Objective-C)
class NSComboBox : NSTextField
{
    private void* _dataSource;

    void insertItem(ObjcObject object, NSInteger value) [insertItemWithObjectValue:atIndex:];
    void insertItem(ObjcObject object) [insertItemWithObjectValue:];
}

comboBox.insertItem(val, idx); // calls insertItemWithObjectValue:atIndex:
comboBox.insertItem(val);      // calls insertItemWithObjectValue:

Defining a Subclass

Creating a subclass from an existing Objective-C class is easy, first we must make sure the base class is declared:

extern (Objective-C)
class NSObject
{
    ...
}

Then we write a derived class as usual:

class WaterBucket : NSObject
{
    float volume;

    void evaporate(float celcius)
    {
        if (celcius > 100)  volume -= 0.5 * (celcius - 100);
    }
}

WaterBucket being a class derived from an Objective-C class, it automatically becomes an Objective-C class itself. We can now pass instances of WaterBucket to any function expecting an Objective-C object.

Note that no Objective-C selector name was specified for the evaporate function above. In this case, the compiler will generate one. If we need the function to have a specific selector name, then we must write it explicitly:

void evaporate(float celcius) [evaporate:]
{
    if (celcius > 100)  volume -= 0.5 * (celcius - 100);
}

If however we were overriding a function present in the base class, or implementing a function from an interface, the Objective-C selector would be inherited.

Constructors

To create a new Objective-C object in Objective-C, one would call the allocator function and then the initializer:

NSObject *o = [[NSObject alloc] init];

In D, we do this instead:

auto o = new NSObject();

The new operator knows how to allocate and initialize an Objective-C object, it only need helps to find the right selector for a given constructor. When declaring an Objective-C class, we can map constructor to selector names:

extern (Objective-C)
class NSSound : NSObject
{
    this(NSURL url, bool byRef) [initWithContentsOfURL:byReference:];
    this(NSString path, bool byRef) [initWithContentsOfFile:byReference:];
    this(NSData data) [initWithData:];
}

Like for member functions, omiting the selector will make the compiler generate one. But if a constructor is inherited from a base class or implements a constructor defined in an interface, it’ll inherit that selector instead.

{Question: What do we do when “virtual” constructors call each other? Should the compiler know about designated initializers?}

Properties

When not given explicit selecectors, property functions are given the appropriate method names so they can participate in key-value coding.

class Value : NSObject
{
    @property BigInt number();
    @property void number(BigInt v);
    @property void number(int v);
}

Given the above code, the compiler will use the selector number for the getter, setNumber: for the setter having the same parameter type as the getter, and the second alternate setter will get the same compiler-generated selector as a normal function.

Objective-C Protocols

Protocols in Objective-C are mapped to interfaces in D. This declares an Objective-C protocol:

extern (Objective-C)
interface NSCoding
{
    void encodeWithCoder(NSCoder aCoder) [encodeWithCoder:];
    this(NSCoder aDecoder) [initWithCoder:];
}

Unlike regular D interfaces, we can define a constructor in an Objective-C protocol.

The protocol than then be implemented in any Objective-C class:

class Cell : NSObject, NSCoding
{
    int value;

    void encodeWithCoder(NSCoder aCoder)
    {
        aCoder.encodeInt(value, "value");
    }

    this(NSCoder aDecoder)
    {
        value = aDecoder.decodeInt("value");
    }
}

{Note: We probably need support for @optional interface methods too.}

Class Methods

Each class in Objective-C is an object in itself that contains a set of methods that relates to the class itself, with no access to instances of that class. The D equivalent is to use a static member function:

extern (Objective-C)
class NSSound : NSObject
{
    static NSSound soundNamed(NSString *name) [soundNamed:];
}

There is one key difference from a regular D static function however. Objective-C class methods are dispatched dynamically on the class object, so they have a this reference to the class they’re being called on. this might be a pointer to a class derived from the one our function was defined in, and through it we can call a static function from that derived class if it overrides one in the current class. Here is an example:

class A : NSObject
{
    static void name() { writeln("A"); }
    static void writeName() { writeln("My name is ", name()); }
}

class B : A
{
    static void name() { writeln("B"); }
}

B.writeName(); // prints "My name is B"

This is not possible with regular static functions in D.

Using the Class Object

In Objective-C, you can get the class object for a given instance by calling the class method:

[instance class]; // return the class object for instance
[NSObject class]; // return the class object for the NSObject type

This works similarily in D:

instance.class; // get the class object for instance
NSObject.class; // get the class object for the NSObject type

The only difference is that D is strongly-typed, which means that x.class returns a different type depending on the type of x.

Inside an instance method, use this.class to get the current class object; omiting this like for other members is not possible as it would make the amgiguous with.

{Question: is x.class a good syntax?}

There is no classinfo property for Objective-C objects.

Categories

With Objective-C it is possible for different compilation units, and even different libraries, to define new methods that will apply to existing classes.

extern (Objective-C)
class NSString : NSObject
{
    wchar characterAtIndex(size_t index) [characterAtIndex:];
    @propety size_t length() [length];
}

class NSString [LastCharacter]
{
    wchar lastCharacter() @property { return characterAtIndex(length-1); }
}

The class NSString [LastCharacter] syntax declares a category named LastCharacter which adds one method to the NSString class.

{Note: this just a draft idea at this stage.}

NSString Literals

D string literals are changed to NSString literals whenever the context requires it. The following Objective-C code:

NSString *str = @"hello";

becomes even simpler:

NSString str = "hello";

This only works for strings literals. If the string comes from a variable, you’ll need to construct the NSString object yourself.

Selector Literals

When you need to express a selector, in Objective-C you use the @selector keyword:

SEL sel = @selector(setObject:forKey:);

In D, you use the selector template defined in the objc module:

SEL sel = selector!"setObject:forKey:";

You can also get the selector of a function this way:

SEL sel = selector!(NSObject.isKindOfClass);

Protocol Literals

When you need to express a protocol, in Objective-C you use the @protocol keyword:

Protocol *p = @protocol(NSCoding);

In D, you use the interface property of the interface:

Protocol p = NSCoding.interface;

Interface Builder Attributes

The @IBAction attribute forces the compiler generate a function selector matching the name of the function, making the function usable as an action in Interface Builder and elsewhere.

The @IBOutlet attribute mark fields that should be available in Interface Builder.

class Controller : NSObject
{
    @IBOutlet NSTextField textField;

    @IBAction void clearField(NSButton sender)
    {
        textField.stringValue = "";
    }
}

Special Considerations

Casts

The cast operator works the same as for regular D objects: if the object you try to cast to is not of the right type, you will get a null refrence.

NSView view = cast(NSView)object;

// produce the same result as:
NSView view = ( object && object.isKindOfClass(NSView.class) ? object : null );

For interfaces, the cast is implemented similarily:

NSCoding coding = cast(NSCoding)object;

// produce the same result as:
NSCoding coding = ( object && object.conformsToProtocol(NSCoding.interface) ? object : null );

The compiler will do emit any runtime check when casting to a base type.

NSObject vs. ObjcObject vs. id

There are two NSObject in Objective-C: NSObject There protocol and NSObject the class. Not all classes are derived from the NSObject class, but they all implement the NSObject protocol.

In D having, an interface and a class with the same name is less practical. So the NSObject protocol is mapped to the ObjcObject interface instead. ObjcObject is defined in the objc module.

Because all Objective-C objects implement ObjcObject (the NSObject protocol), ObjcObject is used as the base type to hold a generic Objective-C object instead. The Objective-C language uses id for that purpose, but id cannot work in D because the correct mapping of selectors requires that we know the class or inteface declaration.

So if you have a generic Objective-C object and you need to call one of its functions, you must first cast it to the right type, like this:

void showWindow(ObjcObject obj)
{
    if (auto window = cast(NSWindow)obj)
        window.makeKeyAndOrderFront();
}

Exceptions

{Question: How to mix the two models? Perhaps we can just ignore the problem…}

Memory Management

Only the reference-counted variant of Objective-C is supported, but reference counting is automated which makes things much easier.

Assigning an Objective-C object to a variable will automatically call the retain function to increase the reference count of the object, and clearing a variable will call the release function on the reference object. Returning a variable from a function will call the autorelease function.

auto a = textField.stringValue; // implicit a.retain()
auto b = a;                     // implicit b.retain()
b = null;                       // implicit b.release()
a = null;                       // implicit a.release()

The compiler can perform flow analysis when optimizing to elide unnecessary calls to retain and release.

Functions in extern (Objective-C) class or interface declarations that return a retained object reference must be marked with the @retained attribute. The @retained attribute is inherited when overriding a function. Most functions do not need this since they return autoreleased objects.

interface NSCopying
{
    @retained
    ObjcObject copyWithZone(NSZone* zone) [copyWithZone:];
}

Note that casting an Objective-C object reference to some other pointer type will break this mechanism. retain and release must be called manually in those cases.

To create a “weak” object reference that does not change the reference count and automatically becomes null when the referenced object is destroyed, use the WeakRef template in the objc module. This is needed to break circular refrences that would prevent memory from being deallocated.

{Note: need to check how to implement auto-nulling WeakRef efficiently.}

Member variables of Objective-C classes defined in a D module are managed by the garbage collector as usual.

{Note: need to check how to implement this with Apple’s Modern Objective-C runtime.}

Null Objects

Because of the way the Objective-C runtime handle dynamic dispatch, calling a function on a null Objective-C object does nothing and return a zero value if the function returns an integral type, or null for a pointer type. Struct return values can contain garbage however.

Do not count on that behaviour in D. While a D compiler will use the Objective-C runtime dispatch mechanism whenever it can, it might also call directly or inline the function when possible.

As a convenience to detect calls to null objects, you can use the -objcnullcheck command line directive to make the compiler emit instructions that check for null before each call to an Objective-C method and throw when it encounters null.

{Question: Is disallowing calls on null objects desirable? How can we ensure memory-safety for struct return values?}

Applying D attributes

You can apply D attributes to Objective-C methods as usual and they’ll have the same effect as on any D function.

abstract, final
pure, nothrow
@safe, @trusted, @system

Type modifiers such as const, immutable, and shared can also be used on Objective-C classes.

Design by Contract, Unit Tests

D features such as unittest, in and out contracts as well as invariant all work as expected when defining Objective-C classes in D.

Note that invariant will only be called upon entering public functions defined in D. External Objective-C function won’t check the invariants since Objective-C is unaware of this feature.

Inner Classes

Objective-C classes defined in D can contain inner classes. You can also derive an inner class from an Objective-C object.

Memory Safety

While the Objective-C language provide no construct to guarenty memory safety, D does. Properly declared external Objective-C objects should be usable in SafeD and provide the same guarenties.

Generated Selectors

When a function has no explicit selector, the compiler generate one in a way that permits function overloading. To this end, a function with one or more arguments will have the type of its arguments mangled inside the selector name. Manglings follows what the type.mangleof expression returns.

For instance, here is the generated selector for these member functions:

int length();                    // generated selector: length
void moveTo(float x, float y);   // generated selector: moveTo_f:f:
void moveTo(double x, double y); // generated selector: moveTo_d:d:
void addSubview(NSView view);    // generated selector: addSubview_4cocoa6appkit6NSView:

You generally don’t need to care about this. To get the selector of a function, use the selector template in the objc module, or set explictily the selector to use.

Blocks

While not stricly speaking part of Objective-C, Apple’s block extension for C and Objective-C is now used at many places through the Mac OS X Objective-C Cocoa APIs. A block is the same thing as a D delegate, but it is stored in a different data structure.

The type of a block in D is expressed using the same syntax as a delegate, except that you must use the __block keyword. If an Objective-C function wants a block argument, you declare it like this:

extern (Objective-C)
class NSWorkspace
{
    void recycleURLs(NSArray urls, void __block(NSDictionary newURLs, NSError error) handler)
        [recycleURLs:completionHandler:];
}

Delegates are implicitly converted to blocks when necessary, so you generally don’t need to think about them.

workspace.recycleURLs(urls, (NSDictionary newURLs, NSError error) {
    if (error == null)
        writeln("success!");
});

Blocks are only available on Mac OS X 10.6 (Snow Leopard) and later.


Comments

Daniel

Very promising!

This would be the incentive I need to write my first Cocoa program… alone the simple feature of function overloading makes a world of difference.

/Daniel


  • © 2003–2024 Michel Fortin.