The D/Objective-C Bridge
There’s an interesting thing about bridging two programming language: you have to learn a lot about the fundamentals of the two languages. As I love to learn, and I’m pretty fond of both D and Objective-C, building that bridge was the natural thing to do for me.
Early Bridge
D is a language capable of accessing C functions directly: you just need to declare them as extern (C)
inside a D code file and they’ll be made available (provided you link to the right libraries afterwards). So basically, since the whole Objective-C runtime is accessible from C calls, it’s pretty easy to do whatever we want with it.
With the Objective-C runtime you can do introspection, or to create new classes and methods. Methods are pretty easy to implement too: D functions are called with the C ABI, which means that they’re callable from C code. A function in D only need to have the right prototype to be able to receive Objective-C messages.
But on the other side, D is a statically-typed language, in the same spirit as C++. This means that very little information is known at runtime about D objects: their name, their members, their methods, almost all of this disappear once the program has been compiled. This also mean that you can’t dynamically add classes or methods like you can do in Objective-C.
So the early version of that bridge was pretty simple: there was a couple of D classes which were wrapping their Objective-C counterpart. For instance, D class NSObject
contained a pointer to a corresponding Objective-C object instance and could use objc_msgSend
to call methods on it. This works, until you try to actually do something useful, like a subclass. Since Objective-C is completely unaware of that D class, it can’t call any of its methods. If you override NSObject
to do something useful, you better not count on Cocoa calling that method: it doesn’t exist to the outside world.
So basically, that method has to be defined as a method of an Objective-C object to work. We’ll get to that in a moment.
Object Posing
The key to a successful bridge in object-oriented languages is to make objects seemingly transferable between the two languages. In the D/Objective-C Bridge, this is done by proxy objects posing an object in the foreign language. There are two kind of proxies in the current implementation of the bridge:
a capsule is an Objective-C object which serves as a proxy to a D object so that it can be sent as a parameter to a function expecting an Objective-C object. The process of creating a capsule for an object is called encapsulation and retrieving the object from a capsule (when a function returns a capsulated object for instance) is called decapsulation.
a wrapper is a D object representing an Objective-C object within D code. When decapsulating an Objective-C object, after checking it isn’t a capsule, a wrapper D object is created to represent that Objective-C object. When encapsulating a D object, if that object is a wrapper, instead of creating a capsule the wrapped Objective-C object is returned.
That process of encapsulation/decapsulation repeated every time an object passes through a cross-language function call, either as an argument or as a return value. Clearly, this becomes tedious fast-enough.
Templates to the Rescue
Just like its cousin C++, D has templates. Templates allows what we call metaprogramming: code which create code. D supports metaprogramming only at compile-time (just like C++), but this allows us to perform a lot of work. Templates are used to encapsulate and decapsulate function arguments and return values as needed depending on their type. For instance, a function which call an Objetive-C method would be like this:
Object opIndex(Object key) {
SEL selector = sel_registerName("objectForKey:".ptr);
id key_id = encapsulate(key);
id result = objc_msgSend(self, selector, key_id);
return decapsulate(result);
}
With the proper template, it now becomes this:
Object opIndex(Object key) {
return invokeObjcSuper!(Object, "objectForKey:")(key);
}
The invokeObjcSuper
template function accepts any number of argument and will properly convert them as needed based on their type. Note that the first example above, the one without the template, is pretty much alike what you can do in plain C.
D templates makes it easy to convert arguments and return values on the fly, but what about receiving method calls from the Objective-C side? Well, there is a template for that too. In D, you can mixin a template’s content in the current context. Used this way, a template can be used to add methods and variables to a class, which is what we use here. Continuing from the previous example, here is how one woud bind opIndex
to the objectForKey:
selector of an Objective-C capsule class:
mixin ObjcBindMethod!(opIndex, Object, "objectForKey:", Object);
Basically, the syntax reads like this: bind method opIndex
to selector objectForKey:
with return type Object
and one argument of type Object
. Mixing in this template creates a receiver method suitable for calling from Objective-C, and a method_info
structure to be given to the Objective-C runtime when initializing the class. Object arguments are decapsulated, and object return types are encapsulated by the binding method, all this is done completely transparently to the programmer.
Exceptions
What about exceptions? Both D and Objective-C have exceptions, but in implementation they’re incompatible. D catch
and finally
statement (as well as scope (failure)
and scope(exit)
and destructors for scope
-qualified objects) won’t be called when the stack is rewinded at the result of an Objective-C exception. The same applies to Objective-C code whenever a D exception is thrown. So when you have D code which calls Objective-C code which calls D code which calls Objective-C code, things can become quite messy when an exception throws in.
Note that Objective-C++, which is a mix of Objecitve-C and C++ and which is supported by Apple has this exact same problem. As of now, this is unresolved for Objective-C++. But the D/Objective-C bridge solves that.
There is an “exception bridge” in the D/Objective-C bridge. The exception bridge catches Objective-C exceptions raised during a call to Objective-C, wrap them into D objects, then throw that object back. The exact same thing is done in reverse: when D code is called from Objective-C, if an exception is thrown, it gets encapsulated into an Objective-C exception, and raised.
The templates for calling Objective-C methods and binding D methods to selectors use the exception bridge to make the process completely transparent. So you don’t have to worry about exceptions when using the D/Objective-C bridge: catch them and throw them as in any D program.
Class Hierarchy
The class hierarchy becomes a funny thing in the D/Objective-C bridge. As stated earlier, the bridge has two kinds of proxy objects: capsules and wrappers. Capsules are Objective-C objects posing for a D object; wrappers are D objects posing an Objective-C object. That’s all convenient, but how does polymorphism works in all that? Let’s look at wrappers first.
Wrappers
Wrappers are descendent of ObjcWrapperObject
(in the objc.wrapper module), but that’s mostly an implementation detail you should ignore. The first wrapper you should worry about is called… NSObject
! The D class NSObject
is a wrapper for the Objective-C class of the same name. Confused? From now on, I’ll call it the NSObject
wrapper, and the object it wraps will be called its Objective-C instance.
The NSObject
wrapper implements methods which simply calls their corresponding methods on its Objective-C instance, methods alike the opIndex
example above using the invokeObjcSuper
template. This wrapper is the base of all wrappers, just like NSObject
is the base of all objects in the Cocoa hierarchy. Therefore other wrapper classes, like NSString
, inherit from NSObject
, and thus inherit all the methods of NSObject
.
When an Objective-C object is decapsulated, the bridge first checks if a corresponding wrapper has been registered (we will get to registration later). If no wrapper class is found, the bridge will search using the Objective-C object’s superclass, super-superclass and so on, until it find a matching wrapper. Eventually, a corresponding wrapper will be found, and a wrapper object will be created. In the “worst-case” senario, a NSObject
wrapper will be returned.
So basically, the NSObject
wrapper can wrap all Objective-C objects. This fits, since all Objective-C objects are instances of NSObject
. And since no more specific wrapper has been registered for a given object, it’s probably that you don’t need to know about the more specific object anyway, so nothing is lost.
Capsules & Subclassing
So capsules are those Objective-C objects posing for D objects inside Objective-C code. The capsule for a regular D object is an instance of the Objective-C class called D_Object
. This class will simply forward method calls for isEqual:
, hash
, and description
to their counterparts in the base D Object class, namely opEquals
, toHash
, and description
(converting types as necessary). Objective-C is is therefore able call these methods on any D class.
Overriding and defining methods is achieved by subclassing. If you override one of the three already bound methods above, you don’t need to do anything special to get them working from Objective-C. If you wish to add methods and want those methods to be accessible from Objective-C, each of them need to be bound in a Objective-C capsule class corresponding to your D class. That capsule class is automatically created on the first use of the ObjcBindMethod
template, using the name of your class as the name of the class in Objective-C, so you should rarely need to worry about the Objective-C class under the hood.1
Now, what about subclassing an existing Objective-C class? That’s easy too. Each wrapper class has a corresponding capsule. The capsule for the NSObject
wrapper is called D_NSObject
, the capsule for NSString
is called D_NSString
, and so on, but you don’t really need to worry about that unless you’re debugging. If you override a method of a wrapper which has been bound to its corresponding Objective-C capsule class, the bridge will dispatch the call to your method. If you wish to bind a new method to an Objective-C selector, you can do it just like you would for other objects, using ObjcBindMethod
. It’s that simple.
There is a notable exception: cluster classes. Cluster classes in Objective-C are classes like NSString
, NSArray
, NSDictionary
, etc. for which the initializer returns a different object. For these classes, you’ll need to do the traditional alloc–init sequence from your D class constructor. If you don’t, you’ll end up with the wrong capsule class, methods won’t be forwarded and you’ll get exceptions when using your class. For other classes, most of them really, you can just call one of the superclass constructors and be done with it.
In some cases, you’ll want Objective-C code to be able to create instances of some of your subclasses. This is particularly interesting for objects you want unserialized from a nib file. For that you need to bind a constructor to an initializer method. This can be done with the ObjcBindInitializer
template, which is used like that:
mixin ObjcBindInitializer!("initWithObject:", Object);
This will bind selector initWithObject:
to the constructor taking one argument of type Object
.
Class Registration
There are two kinds of registrations, which are handled, mostly, in a transparent manner.
When crossing the D/Objective-C bridge, objects are decapsulated on the fly. Capsules are no problem to decapsulate: since they already exist as D objects, the D object can be found easily. But for wrapping Objective-C objects into D wrappers, the bridge must know which D class maps to which Objective-C class. Wrapper classes know how to register themselves with the bridge the first time they’re used, and generally you don’t need to worry about that.2
Registration works the other way around too. For Objective-C to know about a D class, the class has to be registered with, or added to, the Objective-C runtime. Again this is done transparently on the first use of the class. There is a specific case however which may force you to register the class explicitly: when Objective-C code is expected to create an instance of your class. This happens when loading nib files for instance.
So for these cases, you simply need to get the objcClass
property of the object you want to register. YourDClass.objcClass
returns a pointer to the Objective-C class for your D class (which you can ignore), and getting this property will initialize the capsule class and register it both ways.
Future Directions
The biggest challenge yet will be creating bindings for Cocoa as a whole. Creating wrapper classes is still a manual task. As there’s no way to add classes dynamically in D, we can’t create them at runtime by inspecting defined Objective-C classes. Moreover, in D, objects are constructed much differently from Objective-C, and any attempt to bridge that automatically isn’t likely to give good results.
Having to define wrappers manually may seem inconvenient, but it’s also an opportunity to make bindings better suited for D. For instance, the wrappers for NSString
, NSArray
, and NSDictionary
I’ve taken advantage of D operator overloading to make them behave almost exactly like their language-defined counterparts (D arrays and associative arrays), and many method names have been shortened because D supports function overloading.3
There is also a lot of improvements and optimisations to be done to the bridge itself. The method-call bridging routines for instance generate more code than is necessary and can be confusing in a stack trace because of the many template function calls involved.
Today
I’ll finish this by telling you that you can try out the D/Objective-C bridge today. I’ve added it as a project to the project section of my website. The Xcode project includes the bridge, a small part of the Foundation and AppKit frameworks I’ve created wrappers for, and a demo application called Decagon. The whole thing is available under the GPL license, version 3.
Oh, and by the way, if you’re unfamiliar with D, I suggest you take a look at the official website for the D programming language.
-
Be careful about the name of your class when adding Objective-C bindings. In D, you can define two classes with the same name if they are in different modules; Objective-C class names are all in the same namespace so are more likely to clash. You can mixin the
ObjcSubclass
template to specify a different class name for the capsule class. ↩︎ -
I say generally because sometimes, especially when casting objects to derived classes, you may need to make the bridge aware of that particular class prior retrieving that object so that it’ll be wrapped to a more specific wrapper class than what the getter method requires. ↩︎
-
In D, you can use
NSArray
and some other Foundation classes much like you’d use a regular array:NSArray array1 = new NSArray(["test".toNSString()]); // create from D array NSArray array2 = array1 ~ array1; // concatenate array with itself // loop using foreach foreach (index, object; array1) { NSLog("%d: "~ object.toString(), index); } NSMutableArray array3 = new NSMutableArray; array3 = [new Object, new NSObject]; // assign mutable array from D array array3 ~= array2; // concatenate assign
Comments
Great work! The templates does make it look much neater…
GPL licensed? That seems a shame for a library. Unless you plan on selling a version under another license that allows closed-source use.
It seems like this bridge could become a great bridge for people to write high-quality Mac apps in D (which I find to be a more attractive language than Objective-C, though that’s personal preference of course :)
I agree with the first commenter, though, that this looks like great work!
I haven’t decided yet what I’ll do with it, that’s why it’s available under the GPL only. The GPL allows anyone to play with it and to publish what they’ve done with it… which is exactly what you should do with this early version.
When it becomes a viable alternative for writing software, I’ll either switch to a more permissive license or sell a license compatible with closed-source software. I’m only keeping my options open at the moment.
I agree that making it GPL hinders its adoption. I’m curious about D but never had an opportunity to really run with it. I’d be interested in using the framework if I were guaranteed a non-GPL license when/if I’m ready to use the framework but I dislike putting much effort into something that limited what I could do with what I created with it.
Hi Michel,
I’d say the LGPL would be the license you’re looking for if you want to keep it free (as in libre) but make it more permissive.
take a look at rubycocoas use of the new bridgesupport stuff in leopard. i don’t fully understand what exactly this provides to be honest, so i may be completely wrong in suggesting it, but it seems to me like a good basis for generating binding, assuming i understood “The biggest challenge yet will be creating bindings for Cocoa as a whole”
Grayson: I’m glad you’re interested about D and the bridge. As I said in reply to Kevin, it’ll be made available for commercial software development eventually.
I could start selling commercial licenses right away, but at this point of advancement, it would be a rip-off. Or I could release it under LGPL and say goodbye to the possible revenue stream — this may in fact hinder development.
Once the bridge becomes more stable, I’ll make sure commercial developers can use it. Until then, from a commercial developer standpoint, consider it like a technology preview: a peek at a likely, but still a little uncertain future.
Matthew: You’re right: if I eventually decide to not sell licenses for commercial development, LGPL is a likely contender. Right now, I’m keeping my options open.
Lypanov: RubyCocoa works by dynamically creating classes while introspecting the runtime, Bridge Support helps with parts of the framework which you can’t introspect.
The D/Objective-C bridge build classes at compile-time, not at runtime. I could still use introspection to build the class definitions, but in order to avoid code bloat I’ll have to filter many unnecessary things out. And to better fit D, many parts would require manual editing. I may end up using introspection anyway (at compile-time), nothing is really set in stone right now.
Just wanted to point you to the following: http://www.gnu.org/licenses/why-not-lgpl.html