📜 ⬆️ ⬇️

Cocoa Delphi Flavored

Cocoa delphi flavored

There comes a moment in the life of every man when, having looked at the latest global statistics on the use of operating systems, he realizes that the time has come for a big change. It’s not at all necessary to change the house, work and wife, but it’s worth trying to reach an audience that has grown significantly over the last ten years. It will be about development on Delphi for macOS (nee OS X) and how we in the TamoSoft company chose tools, mastered the new, studied, were undermined by mines and enjoyed the process.

Task


Departure point: our main product TamoGraph Site Survey is a tool for inspecting Wi-Fi networks, which allows you to build coverage maps, optimize the placement of access points, create virtual models of signal propagation and do many more useful things for engineers working in this area. TamoGraph works under Windows. Destination point: well, you guessed it. TamoGraph, which would work under macOS.

The product is written mostly in Delphi, individual modules are written in C ++. Why Delphi? (Variants of the question: Is he not dead yet? Are you sick? Language X is much better, and you are retrogrades unable to master the new!) Friends, why we are using the not so fashionable and popular language / environment (according to Tiobe , today is Object Pascal The 11th most popular language is a lot. This is an excellent productivity, and yes, the power of habit, and a fast compiler, but the main reason lies entirely in the field of technology. We just like to write in Delphi, we get the buzz from it. And when the product is written with pleasure and love, it usually works well. So let's not engage in religious controversy, but go directly to the case.

So, from the point of departure (Windows, Delphi) we have to get to the destination by the shortest route (macOS, as yet unknown language / environment). The following main options were considered:
')
1. Redo everything on Xcode using Swift or Objective-C.
2. Redo much of the Xcode using part of the existing Delphi-code in the form of dynamic libraries.
3. Transform most of it in Delphi using the FMX framework (FireMonkey), and write a small part of the code in Objective-C and use it in the form of dynamic libraries.
4. Redo everything on RemObjects Elements using Oxygene, their kind of Object Pascal.

Each option, of course, found many advantages and disadvantages. Xcode is the complete native GUI, the absence of any problems when interacting with the operating system, the mass of sample code and libraries. But, and this is a very big “but”, with all this “in the bundle” there is the need to rewrite a lot of code into another language. RemObjects Elements is also a complete GUI native, very close to Object Pascal language, which means that existing non-GUI code could be used with relatively minor changes. However, none of us have yet tried this tool. And finally, Delphi FMX. One of the advantages is the use of the existing debugged code to its fullest, familiar environment, but non-native controls (although, as it turned out, this is not quite true, more detailed below), possible difficulties in interacting with the macOS API, and many other doubts.

Having deliberately conferred and having carried out some tests, we, as you guessed from the title of this article, stopped at option (3), i.e. Delphi FMX. The possibility of not rewriting a significant part of the code was very attractive. And, to admit, I really didn’t like RemObjects Elements, which I was initially inclined to. So, the choice is made, roll up their sleeves and go ...

Art preparation


Part of the team already at least had experience of close communication with macOS and was well aware of its structure. Some of them were completely newcomers who needed theoretical training. For these purposes, the book Mac OS X and iOS Internals: To the Apple's Core did a good job. As for practice, MacBooks were bought for all those in need, and different versions of macOS were deployed on virtual machines, from 10.9 to the latest 10.12.

The process of debugging programs for macOS on Delphi is different from the usual process for Windows, where, as a rule, you run the debugged program on the same computer where the Delphi environment is running. C macOS is a bit more complicated: first you install it on a machine with macOS Platform Assistant, i.e. an auxiliary application (part of Delphi) that provides deployment and debugging of the application, and from Delphi, already under Windows, you specify the IP address of the machine running the Platform Assistant:

Delphi Platform Assistant


Then you just run your program, which immediately starts working on the Mac. Naturally, it can be debugged exactly the same way as we all used to debug Windows programs.

FMX controls


So, everything is set up, you can run your first “Hello World” on the Mac. The GUI is done in the familiar Delphi visual editor using the FMX visual components. The FMX framework appeared in Delphi back in 2011, in the Delphi XE2 version. I must say that at first he was extremely buggy, but during these six years he was thoroughly rewritten, significantly reducing the number of problems. Now it is quite a usable set of components, starting from the simplest TButton and ending with grids, listview, and other familiar controls. Therefore, it is quite realistic and comfortable to do interfaces on FMX today, however there are some special features.

First, FMX controls are not native. This is not a wrapper around system controls, as is done in the VCL, where, for example, TButton is the system control that Windows draws, not Delphi. Here, Delphi draws controls, using its style engine, which uses a style that matches the style of the version of macOS on which the program is running.
An example of a dialogue on Yosemite (10.10):

Conversation in macOS 10.10

Below is the same dialogue on the Mavericks (10.9). GUI styles automatically adapted to the Mavericks “native” style and look different:

Conversation in macOS 10.9

In principle, this works well, although some things in styles have to be corrected (or use native controls, which is discussed below). For example, the “graphite” style of macOS, which appeared in Yosemite, is absent in Delphi, and it had to be done independently. It took two man-days.

The second problem is “childhood diseases”. The child (FMX framework), as I said, is six years old, and despite the efforts of Embarcadero, he has not fully recovered from all that he needs. For example, in the main menu of the application, the OnClick event is triggered for all items, except top-level items. Those. if you have the File → Open, File → Save menu and so on, then the OnClick event will happen when you click on Open and Save, but it does not happen when you click on File, when the list of sabitems drops out. Or take the standard dialogs Open and Save. Quite unexpectedly, the dialogue display completely “closes” the application's event loop, and something ceases to happen (including the timer ticks) while the dialogue is open. All this, in my opinion, is the result of too weak in-house testing and too slow response from Embarcadero to bug reports.

These diseases are treated at run-time, without patching system units. We cured the absence of OnClick by intercepting the call 'menuWillOpen:' of the class TFMXMenuDelegate, we generally rewrote the system dialogs entirely, but to fix the bug, you must first explode it. Be careful, do not neglect testing, and do not forget to report bugs on quality.embarcadero.com .

Finally, closing the topic of FMX controls, I advise you to take a look at the TMS FMX UI Pack, which includes many very good written visual components, including an excellent TreeView that can work in a virtual mode. This is exactly what is missing from standard FMX components.

Run-time library


Using Delphi RTL expectedly turned out to be the most trouble-free part when porting code to macOS. RTL has long been "sharpened" under multiplatform, so you can safely use any functions and non-visual classes without changes. It is only necessary to monitor such trifles as, for example, the use of the platform-independent IncludeTrailingPathDelimiter instead of the hard-coded "\" separator.

macOS API


When you write something a little more complicated than a calculator, you will sooner or later have to use the native API. Only RTL and the FMX framework are completely unrealistic, just as under Windows, RTL and VCL alone are unrealistic. Need to know the system locale? Implement interprocess communications? Find out the size of the virtual memory process? Encryption? Speech synthesis? All this, of course, native API. But this should not scare at all, as we are not afraid of calling any FindWindow or GetLocaleInfo under Windows. And if something is not declared in Delphi, then you can declare, add and redo anything.

The API itself consists of several components (BSD, Mach, Carbon, Cocoa, etc.), but for our purposes Cocoa is of primary interest. Simply put, Cocoa is a collection of classes, which is rather unusual for those who are used to using the Windows API. For example, if you need to know the offset of the computer's time zone relative to UTC, then in Windows it is just a function GetTimeZoneInformation. But in macOS it is already an NSTimeZone class. Over time, you get used to this by smoking the Apple API Reference at your leisure, just like almost all of us used to smoke MSDN at the beginning of our journey. But what really causes the brain to explode at first is the syntax of the “bridge” between Delphi and Cocoa classes. This is very unusual.

Class functions are called through the magic word OCClass:

TNSTimeZone.OCClass.localTimeZone 

They usually return pointers, but not everything is not so simple. These pointers to objects cannot be used directly; pointers are what is called id in Objective-C, and to convert such a pointer into an object, you need to make magic Wrap:

 TNSTimeZone.Wrap(TNSTimeZone.OCClass.localTimeZone) 

And now we can already call the function of the class instance and finally get the required offset:

 TimeZoneShift:= TNSTimeZone.Wrap(TNSTimeZone.OCClass.localTimeZone).secondsFromGMT; 

Another example? You are welcome! Checking server availability:

 function BlockingGetTestURL: boolean; var URL: NSURL; URLRequest: NSURLRequest; aData: NSData; Response: Pointer; Policy: NSURLRequestCachePolicy; TimeOut: NSTimeInterval; const URL_TO_CHECK = 'http://open.mapquestapi.com'; begin URL := TNSURL.Wrap(TNSURL.OCClass.URLWithString(StrToNSStr(URL_TO_CHECK))); Policy:= NSURLRequestReloadIgnoringLocalCacheData; TimeOut:= 10; URLRequest := TNSURLRequest.Wrap(TNSURLRequest.OCClass.requestWithURL(URL, Policy, TimeOut )); aData := TNSURLConnection.OCClass.sendSynchronousRequest(URLRequest, @Response, nil); result:= (aData <> nil) and (aData.length > 0); end; 

When an Objective-C class function wants a pointer to an object from us, again, we cannot simply take and pass an @MyDelphiObject, we must perform a ritual dance to convert this pointer to id using the GetObjectID function:

 function GetUserDefaultMeasureUnit : TSSMeasureUnitType; var p: pointer; ns: NSString; const AppKitFwk: string = '/System/Library/Frameworks/AppKit.framework/AppKit'; begin ns:= CocoaNSStringConst( AppKitFwk, 'NSLocaleUsesMetricSystem'); p := TNSLocale.Wrap(TNSLocale.OCClass.currentLocale).objectForKey((NS as ILocalObject).GetObjectID); if TNSNumber.Wrap(p).boolValue then Result := msMeters else result:= msFeet; end; 

In general, it is quite possible to get used to the syntax, having studied the examples. I advise you to read the article Using OS X APIs directly from Delphi , in which this topic is well disclosed.

If we talk directly about the API (not limited to Cocoa), it leaves a pretty pleasant feeling. There are simply no things in the Windows API in the macOS API, and vice versa. Some things in macOS are made more complicated than in Windows, some are simpler. Take, for example, AES encryption. In Windows, in order to encrypt an array of bytes, you need to use five functions and a couple dozen lines of code, whereas in macOS you can do this on almost one line with the CCCrypt function. And this is no longer part of Cocoa.

Sweet, sweet POSIX


POSIX isn't part of Cocoa either, but damn it, thanks a lot to him for being on macOS! It makes life so much easier. Much that can be done through classes at a high level is much easier to do at a low level through POSIX. For example, how to implement interprocess communications? Distributed Objects and the NSProxy class? NSConnection? Forget, everything is solved in a couple of lines of code through memory-mapped files and POSIX functions. We need shm_open, shm_unlink and mmap. The first two, by the way, are not declared in Delphi, but this is not a problem. Carefully read the description, declare:

 function shm_open(__name: PAnsiChar; __oflag: integer; __mode: mode_t): integer; cdecl; external libc name _PU + 'shm_open'; function shm_unlink(__name: PAnsiChar): integer; cdecl; external libc name _PU + 'shm_unlink'; 

And then everything is simple, we call:

 fd := shm_open( PAnsiChar(UTF8Encode(ID)), O_RDWR or O_CREAT, S_IRUSR or S_IWUSR or S_IRGRP or S_IROTH ); ftruncate(fd, aSize); mmap(nil, aSize, PROT_READ or PROT_WRITE, MAP_SHARED, fd, 0); 

Everything, we have created a mapping, accessible by name from other processes.

Why do we still need POSIX? Yes, for many things. For example, here:

 function GetPhysicalCoreCount: Cardinal; var CoreCount: Cardinal; Size: Integer; begin Size:= SizeOf(Cardinal); if sysctlbyname('hw.physicalcpu',@CoreCount, @Size, nil, 0) = 0 then result:= CoreCount else result:= System.CPUCount; end; 

Working with sockets, with COM ports, and much more - simple and familiar POSIX is suitable for all of this, with almost the same syntax as in Windows. Among other things, we needed to port the Delphi-class to work with COM ports, which we used under Windows to work with GPS receivers. There are approximately 1500 lines of code. Complicated? Not, no so much. Day of work and about 50 IFDEFs of this type:

 function TGPSReceiver.ClearInputBuffer: Boolean; begin Result := False; if Assigned(ComThread) and ((ComThread as TComThread).ComDevice <> GPS_INVALID_HANDLE_VALUE) then begin try {$IFDEF MSWINDOWS} Result := PurgeComm((ComThread as TComThread).ComDevice, PURGE_RXCLEAR); {$ENDIF} {$IFDEF MACOS} result:= tcflush((ComThread as TComThread).ComDevice, TCIFLUSH) = 0; {$ENDIF} except Result := False; end; end; end; 

Ported, tested, by the end of the working day we got a module for working with GPS that was flashing with images of satellites.

Native controls


If you are not satisfied with the standard set of FMX controls, then it does not matter. Nobody forbids the use of native visual classes and even mix them with FMX controls, following certain rules. As a matter of fact, nobody forbids even to use the FMX framework in your Delphi application at all (although this is already a bit extreme).

Native classes should be used for performance. We, for example, faced with the fact that the viewport, made on FMX components, significantly slowed down when zooming and scrolling large bitmaps, we replaced it with a native NSScrollView with NSImageView inside. To access the events of native classes, they need to subclass and / or use delegates. This is rather trivially encoded in Delphi, and as a result you get access to any events. Need event magnifyWithEvent class NSImageView? No problem. We inherit the interface:

 NSImageViewEx = interface(NSImageView) ['{3E4F87DA-0577-4F21-A1CF-8BCA774FA903}'] procedure magnifyWithEvent(event: NSEvent); cdecl; end; 

Make the implementation class:

 TExtendedNSImageView = class(TOCLocal) … public procedure magnifyWithEvent(event: NSEvent); cdecl; … end; 

And we do everything we want when the method of the implementation class is called:

 procedure TExtendedNSImageView.magnifyWithEvent(event: NSEvent); begin // Do whatever you want end; 

For this to work, you need some more code when creating the class; examples can be easily found on the Internet. Subclassing is not the only way to intercept events, you can also use method swizzling, and I will even give an example below.

This is how we live, mixing the native and FMX controls.

TamoGraph on macOS


What (so far) Delphi can't on macOS


With a large barrel of honey often comes a certain amount of not so beautiful substance. Let's talk for a change about the shortcomings.

Of the unsolvable problems, there is one, but rather important. This is a 64-bit compiler for macOS, which is in the roadmap, but not yet done. This, of course, is a shame for Idera / Embarcadero, who are, in our opinion, passionate about much less important things, neglecting the Mac branch of the product. So, we look forward to it.

From solved - code blocks , language feature ++ and Objective-C, which is not supported in Delphi. More precisely, Delphi has its analogue of code blocks, but it is incompatible with those code blocks that it expects from macOS API. The fact is that many classes have functions that use code blocks as handler's for completion. The simplest example is the beginWithCompletionHandler classes NSSavePanel and NSOpenPanel. The transmitted code block is executed at the moment of closing the dialog:

 - (IBAction)openExistingDocument:(id)sender { NSOpenPanel* panel = [NSOpenPanel openPanel]; // This method displays the panel and returns immediately. // The completion handler is called when the user selects an // item or cancels the panel. [panel beginWithCompletionHandler:^(NSInteger result){ if (result == NSFileHandlingPanelOKButton) { NSURL* theDoc = [[panel URLs] objectAtIndex:0]; // Open the document. } }]; } 

On Delphi, this "trick with the ears" is apparently extremely difficult to perform (at least we did not succeed). In other words, in the normal way we cannot learn about closing the dialogue. But the normal way is even boring! Who prevents us from going abnormal way? There are several perverse approaches to solving such problems, but in this case, for example, the following will work well. For a start, we can get a list of all the functions, both documented and undocumented, of the NSSavePanel class. This is done like this:

 function ListMethodsForClass(const aClassName: string): string; var aClass: Pointer; OutCount, i: integer; Arr: PPointerArray; p: PAnsiChar; begin result:= 'Instance methods for class ' + aClassName + ':' + #13#10; aClass := objc_getClass(PAnsiChar(ansistring(aClassName))); if aClass <> nil then begin Arr:= class_copyMethodList(aClass, OutCount); if Arr <> nil then begin for i := 0 to OutCount - 1 do begin p:= sel_getName(method_getName(Arr^[i])); result:= result + string(p) + #13#10; end; Posix.Stdlib.free(Arr); end; result:= result + 'Class methods:' + #13#10; Arr:= class_copyMethodList(object_getClass(aClass), OutCount); if Arr <> nil then begin for i := 0 to OutCount - 1 do begin p:= sel_getName(method_getName(Arr^[i])); result:= result + string(p) + #13#10; end; Posix.Stdlib.free(Arr); end; end; end; 

Got a list and look for something delicious ... Yeah, found: "_didEndSheet: returnCode: contextInfo:". Very similar to what we need. It is necessary to check the theory whether this selector is called when the dialog is closed. You can make a subclass of NSSavePanel, and you can roughly and shamelessly put a hook on this selector by replacing method sizzling:

 const END_SHEET_SELECTOR : ansistring = '_didEndSheet:returnCode:contextInfo:'; SAVE_PANEL_CLASS : ansistring = 'NSSavePanel'; var endSheetOld: procedure (self: pointer; _cmd: pointer; sheet: pointer; returncode: NSinteger; contextinfo: pointer); cdecl; procedure endSheetNew (self: pointer; _cmd: pointer; sheet: pointer; returncode: NSinteger; contextinfo: pointer); cdecl; begin endSheetOld(self, _cmd, sheet, returncode, contextinfo); FDialogClosed:= ReturnCode; end; procedure DoDialogHooks(); var FM1, aClass: pointer; begin aClass := objc_getClass(PAnsiChar(SAVE_PANEL_CLASS)); if aClass <> nil then begin FM1 := class_getInstanceMethod(aClass, sel_getUid(PAnsiChar(END_SHEET_SELECTOR))); if FM1 <> nil then begin @endSheetOld := method_getImplementation(FM1); method_setImplementation(FM1, @endSheetNew); end else raise Exception.Create('Failed to hook NSSavePanel'); end; end; 

We check - and about a miracle, at the moment of closing the dialogue by Cancel or OK, we find ourselves in the hooked function and, accordingly, we learn that the dialogue is closed, as well as the result of the closing itself.

Mines


Probably no one was able to create a product without exploding mines, but I’ll emphasize it specifically once again, the number of explosions can be minimized if you look more at someone else’s code, read books and API reference. No, really, it's better to read about some App Nap on developer.apple.com , than not to read and then wonder for a long time why all the timers in your application suddenly began to tick 10 times less often. And it is better to know in advance that the string parameters in POSIH functions must be encoded in UTF-8, and not ANSI or UTF-16. And test, test, test ... And "for myself and for that guy." Yes, there will be mines in Delphi too, Idera / Embarcadero doesn’t like to test the Mac part of the product. Well, Macapi.Foundation.NSMakeRect would not have fallen if they had organized the testing normally.

Results


I hope, for those who are thinking about how to make a product for macOS, the first acquaintance with Delphi + Cocoa turned out to be informative. A bunch of quite working, allowing you to do serious software. And my wishes Idera / Embarcadero - do not forget about macOS. I understand that mobile development is very fashionable, but the development of desktop software is a very decent market, as you can see with the example of Windows in the last 20 years. You have almost everything for a great product for macOS, you just need to work a little more. Roll out a 64-bit compiler rather and correct what you were told about at quality.embarcadero.com .

Source: https://habr.com/ru/post/319922/


All Articles