V and Objective-C interoperability
I have been writing a desktop application using V recently. After digging into the V UI code, I was curious about how V interacts with the native OS UI, for example how the native windows and menus are created. This started my journey of learning macOS development, Cocoa, Objective-C and finally led to the question of how to call Objective-C code in V.
This post discusses the options of V and Objective-C interoperability and introduces an open source project for Objective-C Runtime bindings.
1. V and C
Before jumping into Objective-C, let’s take a look how V works with C. When we run the “v .”
or “v run .”
commands, the V compiler generates human readable C code. This makes the V and C interoperation easy and efficient.
The V and C section in the V documentation has an example of using sqlite3
C code in V. Here is a code snippet:
#flag -lsqlite3
#include "sqlite3.h"
struct C.sqlite3 {
}
type FnSqlite3Callback = fn (voidptr, int, &&char, &&char) int
fn C.sqlite3_open(&char, &&C.sqlite3) int
The key takeaways are:
The C compilation flags can be used to link to the shared library and include header files.
The
C.
prefix is used to redeclare C structs and functions in V.V has a few types to interoperate with C types:
voidptr
for C'svoid*
&u8
for C'sbyte*
&char
for C'schar*
&&char
for C'schar**
The same mechanism can be used for V and Objective-C, given Objective-C is a superset of the C programming language. However, the object-oriented capabilities provided by Objective-C cannot be used in V directly. As a result, it’s not straightforward to use Cocoa framework in V. We will explore two options in following sections.
2. Mixing V and Objective-C code
Besides calling C code in a shared library, a V module can have both V and C code. V can call the C code directly. This gives us the idea:
Implement the main logic in Objective-C and expose the APIs using plain C functions.
In the V code, call the exposed C functions.
The macos_tray example in the vlang repo is implemented in this way. Here is an example of an exposed C function written in Objective-C:
// file: tray.m
// Initializes NSApplication and NSStatusItem, aka system tray menu item.
main__TrayInfo *tray_app_init(main__TrayParams params) {
NSApplication *app = [NSApplication sharedApplication];
AppDelegate *appDelegate = [[AppDelegate alloc] initWithParams:params];
// Hide icon from the doc.
[app setActivationPolicy:NSApplicationActivationPolicyProhibited];
[app setDelegate:appDelegate];
[appDelegate initTrayMenuItem];
main__TrayInfo *tray_info = malloc(sizeof(main__TrayInfo));
tray_info->app = app;
tray_info->app_delegate = appDelegate;
return tray_info;
}
The function is called from V in the same module:
// file: tray.v
#include <Cocoa/Cocoa.h>
#flag -framework Cocoa
#include "@VMODROOT/tray.m"
fn C.tray_app_init(TrayParams) &TrayInfo
// Parameters to configure the tray button.
struct TrayParams {
items []TrayMenuItem [required]
on_click fn (item TrayMenuItem)
}
fn main() {
mut my_app := &MyApp{
tray_info: 0
}
my_app.tray_info = C.tray_app_init(TrayParams{
items: [TrayMenuItem{
id: 'hello'
text: 'Hello'
}, TrayMenuItem{
id: 'quit'
text: 'Quit!'
}]
on_click: my_app.on_menu_item_click
})
}
The interoperation is bidirectional. The V struct can be accessed in Objective-C code as well, via the name of <module>__<struct>
. In the example above, the V TrayParams
struct is accessed as main__TrayParams
in the Objective-C code.
This approach requires very little effort to set up. For one who is familiar with Objective-C, they can have something running within minutes. However, it does have some drawbacks IMHO:
For anything to be called in V, it has to be exposed as a plain C function. It’s a tedious job to add the extra code.
The logic is distributed in V and Objective-C code, calling each other. It makes the code less readable and harder to maintain.
3. Objective-C Runtime bindings
The Objective-C Runtime is a library which provides functions and data structures to replicate the Objective-C compiler behaviors during runtime. For example, it has functions to dynamically load a class definition, create an object from it and call methods on it. The runtime library is useful primarily for developing bridges layers between Objective-C and other languages. Many of the functions are written in C.
Since V can easily interoperate with C, we can create V bindings for this Objective-C Runtime library and call the Objective-C code via it. It takes some upfront effort to implement the bindings. Once we have it, we don’t need to write Objective-C code in a V module. It can be written in V purely.
Going further with this idea, I created a V bindings project named objc.
4. The objc project
The objc project provides V bindings to Objective-C Runtime. A basic usage is:
module main
import magic003.objc { class, sel }
#include <Foundation/Foundation.h>
#flag -framework Foundation
fn main() {
// load classes
num_cls := class('NSNumber') or { panic('failed to load class NSNumber') }
array_cls := class('NSMutableArray') or { panic('failed to load class NSMutableArray') }
// create an instance of NSNumber
n1 := num_cls.message(sel('numberWithInt:')).args1[int](1).request[objc.Id]()
// create an instance of NSMutableArray
arr := array_cls.message(sel('new')).request[objc.Id]()
// add a number to the array
arr.message(sel('addObject:')).args1(n1).notify()
}
It typically takes 3 steps to use a class in Objective-C:
Load the class.
Create an object from the class.
Call methods on the object via messages.
Now, let’s see how these key features are implemented in the objc module.
4.1 Class, Selector and Id
In Objective-C Runtime, the class, method selector and id
are defined as pointers to structs:
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// A pointer to an instance of a class.
typedef struct objc_object *id;
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
In the V bindings, we don’t really care of the actual struct type. All we need is a pointer to them. For such data structures in Objective-C Runtime, we create a wrapper which keeps a voidptr
pointer to the object. Here is an example of Class
. It calls the objc_getClass function to gets the instance of a class.
// A type that represents an Objective-C class.
[noinit]
pub struct Class {
ptr voidptr [required] // the Objective-C Class object
}
// Class.get returns the class definition of a specified class.
// It returns `none` if the class is not registered
// with the Objective-C runtime.
pub fn Class.get(name string) ?Class {
unsafe {
class := C.objc_getClass(&char(name.str))
if class != nil {
return Class{class}
}
return none
}
}
The method selector is similar and the code can be found here.
The id
is implemented slightly differently, though I tried to use the same way. It is often used as the function return type and passed directly to objc_msgSend*
functions. I tried to wrap it in an Id
struct and unwrap it before calling the objc_msgSend*
functions, but didn’t work because the generic type inference still thinks the return type is the Id
struct. Not quite sure if it’s a bug of generics in V or I didn’t do it correctly. So it is defined as a type alias instead:
// A pointer to an instance of a class.
// It represents the `id` type in Objective-C.
type Id = voidptr
4.2 Sending messages
In Objective-C, calling methods on an object is achieved via sending messages. The Runtime provides several functions, for example objc_msgSend and objc_msgSend_stret. Providing the object, method selector and arguments, it calls the particular method on the object.
There are challenges implementing it in V. First, the objc_msgSend*
are primitive functions as shown below. They must be cast to an appropriate function pointer type before being called. This article explains it really well.
void objc_msgSend(void /* id self, SEL op, ... */ )
void objc_msgSend_stret(void /* id self, SEL op, ... */ )
We couldn’t simply redeclare the objc_msgSend*
functions in V and call them to send the messages. They must be cast to the correct function types based on argument and return types, which are unknown until the usage. That’s when we need generics.
The MsgBuilder
is used to construct and send a message. The arguments and return value are defined as generic types.
// A message builder.
[noinit]
struct MsgBuilder {
id Id [required]
op Sel [required]
}
// args1 adds 1 argument to the message.
pub fn (m MsgBuilder) args1[A](a A) Msg1[A] { /* ... */ }
// args2 adds 2 arguments to the message.
pub fn (m MsgBuilder) args2[A, B](a A, b B) Msg2[A, B] { /* ... */ }
// request sends a message requesting a value of type `R`.
pub fn (m MsgBuilder) request[R]() R { /* ... */ }
When sending the message, it cast the functions based on the generic types. Here is the example for a message with two arguments:
// Cast objc_msgSend* functions to this function for 2 arguments and
// return type `R`.
type FnSendMsg2[R, A, B] = fn (voidptr, voidptr, A, B) R
// send_msg_2 calls objc_msgSend* function for 2 arguments and
// return type `R`.
fn send_msg_2[R, A, B](id Id, op Sel, a A, b B) R {
msg_send_fn := get_msg_send_fn[R]()
casted_fn := unsafe { FnSendMsg2[R, A, B](msg_send_fn) }
return casted_fn[R, A, B](id, op.ptr, a, b)
}
Thanks to type inference, most of the time we only need to explicitly specify the type parameter for return values, and V will infer the generic types for arguments. Please refer to the objc
examples.
The second challenge is to determine which objc_msgSend*
function to call. The objc_msgSend function returns the value in registers, while objc_msgSend_stret returns structs on the stack. Based on the tests on my laptop, what matters is actually the size of the return type. Even for a struct, if it is small enough to be saved in a register, the objc_msgSend function must be used. Starting with a simple heuristic, it uses the C pointer size as the threshold:
// get_msg_send_fn determines which objc_msgSend* function to call
// based on `R`.
fn get_msg_send_fn[R]() FnSendMsgGeneric {
// WARNING: this is a very naive way to decide calling
// objc_msgSend or objc_msgSend_stret.
// If the size of the return type is less or equal than
// the C pointer size, it assumes the value
// can be saved in registers and hence the objc_msgSend
// is used. Otherwise, objc_msgSend_stret is used.
// It is only tested on x86_64 and may not work on other
// architecture.
ptr_size := sizeof(voidptr)
if sizeof(R) <= ptr_size {
return C.objc_msgSend
} else {
return C.objc_msgSend_stret
}
}
4.3 Next steps
The objc project is still experimental. It has the bare minimal features to create objects and call methods on them. It will add more APIs and support creating new classes in the future.