DIP43

From D Wiki
Revision as of 14:39, 28 December 2014 by Doob (talk | contribs) (Update to latest selector syntax)
Jump to: navigation, search

DIP 43: D/Objective-C

Title: D/Objective-C
DIP: 43
Version: 1
Status: Draft
Created: 2013-06-29
Last Modified: 2013-06-29
Author: Michel Fortin, Jacob Carlborg
Links: Objective-C

Abstract

This document is an overview of the extensions D/Objective-C brings to the D programming language. It assumes some prior knowledge of Objective-C.

Note: Some parts of this document describe features which are not yet implemented and are very much subject to change. Unimplemented sections of this document are marked as such.

Rationale

Currently it's very cumbersome and verbose to use Objective-C libraries from D. The reason to use these libraries is to be able to use the system frameworks on Mac OS X and iOS. This proposal adds language extensions to make it significantly easier to interact and create libraries compatible with Objective-C.

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 coming 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 reachable 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 accomplished 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) @selector("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) @selector("insertItemWithObjectValue:atIndex:");
    void insertItem(ObjcObject object) @selector("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) @selector("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) @selector("initWithContentsOfURL:byReference:");
    this(NSString path, bool byRef) @selector("initWithContentsOfFile:byReference:");
    this(NSData data) @selector("initWithData:");
}

Like for member functions, omitting 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.

Properties

When not given explicit selectors, 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) @selector("encodeWithCoder:");
    this(NSCoder aDecoder) @selector("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) @selector("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.

Class References

In Objective-C, you can get a reference to a class 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 similarly 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; you cannot omit this like you can for regular members as it would be ambiguous for the parser.

There is no classinfo property for Objective-C objects.

Class Extensions (also known as Categories) {unimplemented}

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) @selector("characterAtIndex:");
    @propety size_t length() @selector("length");
}

extern (Objective-C)
__classext LastCharacter : NSString
{
    wchar lastCharacter() @property;
}

unittest
{
    NSString s = "hello";
    assert(s.lastCharacter == 'o');
}

The __classext LastCharacter : NSString syntax maps to an Objective-C class extension named LastCharacter adding methods to the NSString class. Methods in the extension are dispatched dynamically, so you can override them in a subclass of NSString, or in an extension of that subclass.

Having two extensions defining a function with the same selector will make the Objective-C runtime use one of the two implementations in both cases.

{Question: should we mangle the extension name in the selector to avoid conflicts? This would transparently implement Apple's recommendation that methods in third-party extensions should use a prefix to avoid clashes with future versions of the extended class and other extensions.}

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";

Automatic conversion 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(hasSuffix:);

In D, selectors are type-safe. To create a selector type, you must know the return type and the parameter type this selector should have. You can then

BOOL __selector(NSString) sel = &NSString.hasSuffix;

A selector type can be used just like a delegate, with one difference. When calling a selector, you need to add the object this selector applies to as the first argument:

NSString s = "hello world";
sel(s, "world"); // same as s.hasSuffix("world")

Protocol References

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

Protocol *p = @protocol(NSCoding);

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

Protocol p = NSCoding.protocolof;

Interface Builder Attributes {unimplemented}

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 reference.

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 similarly:

NSCoding coding = cast(NSCoding)object;

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

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

NSObject vs. ObjcObject vs. id

There are two NSObject in Objective-C: NSObject the 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.

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 interface 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

Exceptions are bridged between Objective-C and D code. Since the two languages use a different exception handling mechanism, the compiler has to track which function can throw which kind of exception. It works this way:

1. Function with extern(Objective-C) linkage are assumed to throw using the

  Objective-C mechanism.

2. Any other function is assumed to throw using the D exception mechanism.

You can put an Objective-C type in a catch clause to catch an Objective-C exception inside your D program:

try
    NSLog("%@", obj);
catch (NSException e)
    writeln(e.description);

Note that when propagating in D code, Objective-C exceptions are wrapped inside a Throwable object, so if you catch Throwable you'll also catch Objective-C exceptions. Similarly, Objective-C code will see any thrown D object wrapped inside an instance of NSException, so Objective-C code will be able to catch your exceptions.

Memory Management {unimplemented}

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) @selector("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 references 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 {unimplemented}

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 behavior 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 them.

Global Functions

extern(Objective-C) global functions use the same ABI as C functions, with the exception that any exception thrown from Objective-C functions are assumed to use Objective-C's exception handling.

Inner Classes {unimplemented}

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 guaranty memory safety, D does. Properly declared external Objective-C objects should be usable in SafeD and provide the same guaranties.

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. Mangling 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_C4cocoa6appkit6NSView:

You generally don't need to care about this. To get the selector of a function, take its address and simply assign it to a selector variable:

    void __selector(NSView view) sel = &NSView.addSubview;

Blocks {unimplemented}

While not strictly 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 roughly 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)
        @selector("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.

Breakage

There should be minimal breakage since most changes are new and only affects code marked with extern (Objective-C). A few cases can break where new keywords are introduced, like __selector, __classext (unimplemented) and __block (unimplemented). All these keywords start with two underscores, which are considered reserved by the compiler. That means, these names shouldn't be present in user code.

Copyright

This document has been placed in the Public Domain.