Obj-C interop and D without druntime code to copy/paste

Posted 2019-02-25

More DIP talk in the forums, but I basically ignored all that and played with the new Objective-C interop and some runtime-less D code this week.

Core D Development Statistics

In the community

Community announcements

See more at the announce forum.

New Toys

Objective-C from D

In the new dmd beta, there is better Objective-C interop support. I played with it a little bit, porting an old hello world program to it. I hit some segfaults - new beta code tends to be fragile - but I nevertheless got a program to work. Take a look:

(this code is much bigger than it has to be; I don't actually understand much about programming the Mac, so I copy/pasted some old code from Jacob Carlborg and just adapted that to the new compiler. The goal: pop up a window.)

1 import core.stdc.stdio;
2 
3 void main() {
4 	auto delegate_ = AppDelegate.alloc.init;
5 
6 	NSApp.delegate_ = cast(void*) delegate_;
7 	NSApp.setActivationPolicy(NSApplicationActivationPolicy.regular);
8 	NSApp.run();
9 }
10 
11 /// leaks memory
12 NSString toNSString(string str) {
13 	return NSString.alloc.initWithBytes(
14 	cast(immutable(ubyte)*) str.ptr,
15 	str.length,
16 	NSStringEncoding.NSUTF8StringEncoding
17 	);
18 }
19 
20 extern (Objective-C)
21 class NSApplication : NSResponder {
22 	static NSApplication shared_() @selector("sharedApplication");
23 
24 	void* delegate_() @selector("delegate");
25 	void delegate_(void*) @selector("setDelegate:");
26 
27 	bool setActivationPolicy(NSApplicationActivationPolicy activationPolicy) @selector("setActivationPolicy:");
28 
29 	// use `int` as workaround for https://github.com/ldc-developers/ldc/issues/2387
30 	void activateIgnoringOtherApps(int flag) @selector("activateIgnoringOtherApps:");
31 
32 	void run() @selector("run");
33 }
34 
35 extern (Objective-C)
36 __gshared NSApplication NSApp_;
37 
38 NSApplication NSApp() {
39 	if(NSApp_ is null)
40 		NSApp_ = NSApplication.shared_;
41 	return NSApp_;
42 }
43 alias char* SEL;
44 
45 extern (Objective-C)
46 class NSResponder : NSObject {
47 	NSMenu menu() @selector("menu");
48 	void menu(NSMenu menu) @selector("setMenu:");
49 }
50 
51 
52 extern (Objective-C)
53 class NSNotification {
54 }
55 
56 extern (Objective-C)
57 class NSObject {
58 	NSObject init() @selector("init");
59 	static NSObject alloc() @selector("alloc");
60 }
61 extern (Objective-C)
62 class NSString {
63 	NSString init() @selector("init");
64 	static NSString alloc() @selector("alloc");
65 
66 	NSString initWithBytes(
67 		const(ubyte)* bytes,
68 		NSUInteger length,
69 		NSStringEncoding encoding
70 	) @selector("initWithBytes:length:encoding:");
71 }
72 
73 enum NSBackingStoreType : size_t {
74 	retained = 0,
75 	nonretained = 1,
76 	buffered = 2
77 }
78 
79 extern (Objective-C)
80 class NSMenu : NSObject {
81 	override NSMenu init() @selector("init");
82 	NSMenu init(NSString title) @selector("initWithTitle:");
83 
84 	void setSubmenu(NSMenu menu, NSMenuItem item) @selector("setSubmenu:forItem:");
85 	void addItem(NSMenuItem newItem) @selector("addItem:");
86 
87 	NSMenuItem addItem(
88 		NSString title,
89 		SEL selector,
90 		NSString charCode
91 	) @selector("addItemWithTitle:action:keyEquivalent:");
92 
93 	override static NSMenu alloc() @selector("alloc");
94 }
95 
96 extern (Objective-C)
97 class NSMenuItem : NSObject {
98 	override NSMenuItem init() @selector("init");
99 	override static NSMenuItem alloc() @selector("alloc");
100 
101 	NSMenuItem init(
102 		NSString title,
103 		void* selector,
104 		NSString charCode
105 	) @selector("initWithTitle:action:keyEquivalent:");
106 
107 	extern (D) final
108 	NSMenuItem init(string title, const(char)* selector, string charCode) {
109 		return init(title.toNSString, sel_registerName(selector), charCode.toNSString);
110 	}
111 }
112 
113 version(none)
114 extern (Objective-C)
115 class NSApplicationDelegate : NSObject {
116 	override NSApplicationDelegate init() @selector("init");
117 	override static NSApplicationDelegate alloc() @selector("alloc");
118 	void applicationDidFinishLaunching(NSNotification notification) @selector("applicationDidFinishLaunching:");
119 
120 	bool applicationShouldTerminateAfterLastWindowClosed(NSNotification notification) @selector("applicationShouldTerminateAfterLastWindowClosed:");
121 
122 }
123 
124 
125 extern (Objective-C)
126 enum NSApplicationActivationPolicy : ptrdiff_t {
127 	/* The application is an ordinary app that appears in the Dock and may have a user interface.  This is the default for bundled apps, unless overridden in the Info.plist. */
128 	regular,
129 
130 	/* The application does not appear in the Dock and does not have a menu bar, but it may be activated programmatically or by clicking on one of its windows.  This corresponds to LSUIElement=1 in the Info.plist. */
131 	accessory,
132 
133 	/* The application does not appear in the Dock and may not create windows or be activated.  This corresponds to LSBackgroundOnly=1 in the Info.plist.  This is also the default for unbundled executables that do not have Info.plists. */
134 	prohibited
135 };
136 
137 
138 extern (Objective-C)
139 enum NSStringEncoding : NSUInteger {
140 	NSASCIIStringEncoding = 1,		/* 0..127 only */
141 	NSNEXTSTEPStringEncoding = 2,
142 	NSJapaneseEUCStringEncoding = 3,
143 	NSUTF8StringEncoding = 4,
144 	NSISOLatin1StringEncoding = 5,
145 	NSSymbolStringEncoding = 6,
146 	NSNonLossyASCIIStringEncoding = 7,
147 	NSShiftJISStringEncoding = 8,		  /* kCFStringEncodingDOSJapanese */
148 	NSISOLatin2StringEncoding = 9,
149 	NSUnicodeStringEncoding = 10,
150 	NSWindowsCP1251StringEncoding = 11,	/* Cyrillic; same as AdobeStandardCyrillic */
151 	NSWindowsCP1252StringEncoding = 12,	/* WinLatin1 */
152 	NSWindowsCP1253StringEncoding = 13,	/* Greek */
153 	NSWindowsCP1254StringEncoding = 14,	/* Turkish */
154 	NSWindowsCP1250StringEncoding = 15,	/* WinLatin2 */
155 	NSISO2022JPStringEncoding = 21,		/* ISO 2022 Japanese encoding for e-mail */
156 	NSMacOSRomanStringEncoding = 30,
157 
158 	NSUTF16StringEncoding = NSUnicodeStringEncoding,	  /* An alias for NSUnicodeStringEncoding */
159 
160 	NSUTF16BigEndianStringEncoding = 0x90000100,		  /* NSUTF16StringEncoding encoding with explicit endianness specified */
161 	NSUTF16LittleEndianStringEncoding = 0x94000100,	   /* NSUTF16StringEncoding encoding with explicit endianness specified */
162 
163 	NSUTF32StringEncoding = 0x8c000100,
164 	NSUTF32BigEndianStringEncoding = 0x98000100,		  /* NSUTF32StringEncoding encoding with explicit endianness specified */
165 	NSUTF32LittleEndianStringEncoding = 0x9c000100		/* NSUTF32StringEncoding encoding with explicit endianness specified */
166 }
167 
168 alias NSUInteger = size_t;
169 alias NSInteger = ptrdiff_t;
170 
171 extern (Objective-C)
172 enum NSWindowStyleMask : size_t {
173 	borderless = 0,
174 	titled = 1 << 0,
175 	closable = 1 << 1,
176 	miniaturizable = 1 << 2,
177 	resizable	= 1 << 3,
178 
179 	/* Specifies a window with textured background. Textured windows generally don't draw a top border line under the titlebar/toolbar. To get that line, use the NSUnifiedTitleAndToolbarWindowMask mask.
180 	 */
181 	texturedBackground = 1 << 8,
182 
183 	/* Specifies a window whose titlebar and toolbar have a unified look - that is, a continuous background. Under the titlebar and toolbar a horizontal separator line will appear.
184 	 */
185 	unifiedTitleAndToolbar = 1 << 12,
186 
187 	/* When set, the window will appear full screen. This mask is automatically toggled when toggleFullScreen: is called.
188 	 */
189 	fullScreen = 1 << 14,
190 
191 	/* If set, the contentView will consume the full size of the window; it can be combined with other window style masks, but is only respected for windows with a titlebar.
192 	 Utilizing this mask opts-in to layer-backing. Utilize the contentLayoutRect or auto-layout contentLayoutGuide to layout views underneath the titlebar/toolbar area.
193 	 */
194 	fullSizeContentView = 1 << 15,
195 
196 	/* The following are only applicable for NSPanel (or a subclass thereof)
197 	 */
198 	utilityWindow			= 1 << 4,
199 	docModalWindow		 = 1 << 6,
200 	nonactivatingPanel		= 1 << 7, // Specifies that a panel that does not activate the owning application
201 	hUDWindow = 1 << 13 // Specifies a heads up display panel
202 }
203 
204 struct CGColor;
205 alias CGColorRef = CGColor*;
206 
207 alias CGFloat = double;
208 
209 struct NSPoint {
210 	CGFloat x;
211 	CGFloat y;
212 }
213 
214 struct NSSize {
215 	CGFloat width;
216 	CGFloat height;
217 }
218 
219 struct NSRect {
220 	NSPoint origin;
221 	NSSize size;
222 }
223 
224 
225 extern (Objective-C)
226 class NSWindow {
227 	NSWindow init() @selector("init");
228 
229 	static NSWindow alloc() @selector("alloc");
230 
231 	NSWindow initWithContentRect(
232 	NSRect contentRect,
233 	NSWindowStyleMask style,
234 	NSBackingStoreType bufferingType,
235 	bool flag
236 	) @selector("initWithContentRect:styleMask:backing:defer:");
237 
238 	void makeKeyAndOrderFront(void* sender) @selector("makeKeyAndOrderFront:");
239 	NSView contentView() @selector("contentView");
240 	void orderFrontRegardless() @selector("orderFrontRegardless");
241 	void center() @selector("center");
242 	void contentView(NSView view) @selector("setContentView:");
243 
244 	NSString title() @selector("title");
245 	void title(NSString value) @selector("setTitle:");
246 
247 	/*
248 	extern (D) final
249 	{
250 	void title(string value) {
251 		this.title = value.toNSString;
252 	}
253 	}
254 	*/
255 }
256 
257 
258 extern (Objective-C)
259 class NSView {
260 	NSView init() @selector("init");
261 	NSView initWithFrame(NSRect frameRect) @selector("initWithFrame:");
262 
263 	void addSubview(NSView view) @selector("addSubview:");
264 
265 	bool wantsLayer() @selector("wantsLayer");
266 	// use `int` as workaround for https://github.com/ldc-developers/ldc/issues/2387
267 	void wantsLayer(int value) @selector("setWantsLayer:");
268 
269 	CALayer layer() @selector("layer");
270 	void uiDelegate(NSObject) @selector("setUIDelegate:");
271 }
272 
273 extern (Objective-C)
274 class NSViewController : NSObject {
275 	NSView view() @selector("view");
276 	void view(NSView view) @selector("setView:");
277 
278 	void loadView() @selector("loadView");
279 
280 	void viewDidLoad() @selector("viewDidLoad");
281 }
282 
283 
284 extern (Objective-C)
285 class CALayer {
286 	CGFloat borderWidth() @selector("borderWidth");
287 	void borderWidth(CGFloat value) @selector("setBorderWidth:");
288 
289 	CGColorRef borderColor() @selector("borderColor");
290 	void borderColor(CGColorRef) @selector("setBorderColor:");
291 }
292 
293 extern (Objective-C) class ViewController : NSViewController {
294 	override void loadView() @selector("loadView") {
295 		printf("loadView\n");
296 	}
297 
298 	override void viewDidLoad() @selector("viewDidLoad") {
299 		printf("viewDidLoad\n");
300 	}
301 
302 	override ViewController init() @selector("init");
303 	override static ViewController alloc() @selector("alloc");
304 }
305 
306 extern (Objective-C) class AppDelegate : NSObject {
307 	NSWindow window;
308 
309 	override static AppDelegate alloc() @selector("alloc");
310 	override typeof(this) init() @selector("init");
311 
312 	ViewController controller;
313 
314 	bool applicationShouldTerminateAfterLastWindowClosed(NSNotification notification) @selector("applicationShouldTerminateAfterLastWindowClosed:") {
315 		return true;
316 	}
317 
318 	void applicationDidFinishLaunching(NSNotification notification) @selector("applicationDidFinishLaunching:") {
319 		NSApp.menu = mainMenu();
320 
321 		immutable style = NSWindowStyleMask.resizable |
322 			NSWindowStyleMask.closable |
323 			NSWindowStyleMask.miniaturizable |
324 			NSWindowStyleMask.titled;
325 
326 		auto window = NSWindow.alloc.initWithContentRect(
327 			NSMakeRect(10, 10, 300, 300),
328 			style,
329 			NSBackingStoreType.buffered,
330 			false
331 		);
332 
333 		//window.title = "D Rox";
334 
335 		auto controller = ViewController.alloc.init;
336 		window.contentView = controller.view;
337 
338 		window.center();
339 
340 		this.window = window;
341 		this.controller = controller;
342 
343 		window.makeKeyAndOrderFront(null);
344 		NSApp.activateIgnoringOtherApps(true);
345 	}
346 }
347 
348 NSMenu mainMenu() {
349 	auto mainMenu = NSMenu.alloc.init("MainMenu".toNSString);
350 
351 	auto title = "Apple";
352 	auto menu = NSMenu.alloc.init(title.toNSString);
353 
354 	auto item = mainMenu.addItem(title.toNSString, null, "".toNSString);
355 	mainMenu.setSubmenu(menu, item);
356 
357 	//menu.addItem(NSMenuItem.alloc.init("Quit", "stop:", "q"));
358 
359 	return mainMenu;
360 }
361 
362 pragma(inline, true) NSPoint NSMakePoint(CGFloat x, CGFloat y) {
363 	NSPoint p;
364 	p.x = x;
365 	p.y = y;
366 	return p;
367 }
368 
369 pragma(inline, true) NSSize NSMakeSize(CGFloat w, CGFloat h) {
370 	NSSize s;
371 	s.width = w;
372 	s.height = h;
373 	return s;
374 }
375 
376 pragma(inline, true) NSRect NSMakeRect(CGFloat x, CGFloat y, CGFloat w, CGFloat h) {
377 	NSRect r;
378 	r.origin.x = x;
379 	r.origin.y = y;
380 	r.size.width = w;
381 	r.size.height = h;
382 	return r;
383 }
384 
385 extern(C) void* sel_registerName (const(char)* str);

Compile:

dmd test.d -L-framework -LCocoa -g -debug

Note that you need the new dmd beta version for this.

And run the program! On my box at least, it popped up a window! And then I was able to close it. Yay! lol. There's code in there for the menu and some strings that used to work, but I haven't been able to fix it for the new dmd yet.

It also happens to compile quickly! Pretty exciting - this will make D, technically speaking at least, a real competitor for Objective-C and Swift. Maybe I can make this Mac semi useful to me (I kinda doubt it though, lol, I am not a fan of Macs, but I do wanna port simpledisplay.d to Cocoa anyway.)

No Runtime D

Over the last year or two, D has gotten easier to use without a runtime. Most people talk about -betterC, but you don't actually need that.

In the past, you would have to write at least ~30 lines of code in object.d to get a program running with minimal runtime. But now, it needs virtually nothing. Give this a try (code written for 32 bit Linux):

1 // dmd -c -m32 test.d -defaultlib=
2 // ld test.o -o test  -nostdlib  -m elf_i386
3 
4 void write(in char[] msg) {
5 	auto p = msg.ptr;
6 	auto l = msg.length;
7 	asm {
8 		mov EAX, 4; // write
9 		mov EBX, 1;
10 		mov ECX, p;
11 		mov EDX, l;
12 		int 0x80;
13 	}
14 }
15 
16 struct Foo {
17 	~this() {
18 		write("hi\n");
19 	}
20 }
21 
22 void mymain() {
23 	Foo foo;
24 }
25 extern(C)
26 void _start() {
27 	mymain();
28 	//assert(0, "hi");
29 	asm { mov EAX, 1; mov EBX, 0; int 0x80; }
30 }

The commented assert is to make a point: assert calls a runtime function. Without -betterC, it calls _d_assert_msg. With -betterC, it calls __assert. But in both cases, you need to implement that yourself if you want to use it, since it is a runtime function.

But, to use a minimal runtime, just make an empty object.d, and compile without a library:

$ touch object.d # makes the empty object.d file - no contents required!
# then we will compile and link separately, to exclude all runtime libraries
$  dmd -c -m32 test.d -defaultlib= && ld test.o -o test  -nostdlib  -m elf_i386 && ./test
hi
# and check out how static and small it is!
$ ls -lh ./test
-rwxr-xr-x 1 me users 1.1K Feb 25 12:24 ./test
$ ldd test
        not a dynamic executable

Regardless, without that assert, the rest of the code works - interestingly, a struct with a dtor - with basically zero work. You just create an empty file to represent the runtime, then link without it (which is a bit more complex of a command, but excluding even the C runtime always requires a slightly special command).

This program could even be loaded by GRUB and run on bare metal. (Of course, if you do that, the asm to call into the Linux kernel to say hi will need to change.)

If you needed more of D, you can start add runtime support to your object.d and build up as you need it. And if you don't need it, no need even for a C library, you can make tiny programs now.

The old days of TypeInfo being implicitly required are in the past.