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