Converting from a Cocoa string to a Delphi string

Reading Malcolm Groves’ useful little post about retrieving special directories in OS X, I noticed he converted from Cocoa to Delphi strings by calling the former’s UTF8String function. Given Cocoa strings are already UTF-16 encoded just like Delphi strings, this would cause a conversion roundtrip (UTF-16 -> UTF-8 -> UTF-16). What we really want is a simple data copy – but how?

To give a bit of background, Cocoa is OS X’s object-oriented API, written in – and designed for – Objective-C, a funky-for-the-uninitiated OOP superset of C. For interfacing with Cocoa, the XE 2 RTL provides an Objective-C to Delphi bridge, which leverages the compiler’s RTTI and generics support. In this, Cocoa classes and metaclasses are exposed via interface wrapper types; thus, corresponding to the NSString class in Objective-C is the NSString interface in Delphi. Since NSString has a getCharacters method for copying character data into a buffer, our problem should therefore be easy to solve.

If you turn to Macapi.Foundation.pas, you will find the NSString interface, with getCharacters declared as thus:

procedure getCharacters(buffer: unichar); cdecl; overload;
procedure getCharacters(buffer: unichar; range: NSRange); cdecl; overload;

According to the Apple docs, the single parameter version is deprecated, though that isn’t a big deal since NSRange is just a simple record (you use it to specify the zero-based start index and the number of unichars to return). More annoyingly, the unichar type in Delphi has been mapped to Int16 rather than WideChar – while perhaps not strictly a bug since they are both the same size, it is still a bad choice nonetheless IMO. However, there is a definite bug here as well: ‘buffer’ should be declared as a pointer to a unichar (i.e., PWideChar), not unichar itself! As it stands, this typo makes getCharacters uncallable.

To fix this, you have a couple of choices. The first is to copy the definition of NSString to your own unit, rename it to (say) FixNSString, before amending the getCharacters signatures to look like this:

procedure getCharacters(buffer: PWideChar); cdecl; overload;
procedure getCharacters(buffer: PWideChar; range: NSRange); cdecl; overload;

So long as you haven’t altered the interface’s GUID, you can then just use a typecast  to FixNSString as appropriate:

function NSToDelphiString(const NSStr: NSString): string;
var
  Range: NSRange;
begin
  Range.location := 0;
  Range.length := NSStr.length;
  SetLength(Result, Range.length);
  FixNSString(NSStr).getCharacters(PWideChar(Result), Range);
end;

The other alternative is to make use of the fact NSString has a parallel straight C-compatible type in the ‘Core Foundation’ API layer that (in Apple’s terminology) is ‘toll-free bridged’ with it. This means you can do a hard cast between the two types (NSString and CFStringRef) and use any of their helper functions interchangeably.

Happily, the import declaration of CFStringGetCharacters in Macapi.CoreFoundation.pas is written correctly. Since an NSString instance in Delphi is actually a wrapper to the Objective-C object not that object itself, things are slightly more convoluted than in Objective-C itself, but not that much:

uses Macapi.CoreFoundation, Macapi.ObjectiveC, Macapi.CocoaTypes, Macapi.Foundation;

function CFToDelphiString(const CFStr: CFStringRef): string;
var
  Range: CFRange;
begin
  Range.location := 0;
  Range.length := CFStringGetLength(CFStr);
  SetLength(Result, Range.length);
  if Range.length = 0 then Exit;
  CFStringGetCharacters(CFStr, Range, PWideChar(Result));
end;

function NSToDelphiString(const NSStr: NSString): string; inline;
begin
  Result := CFToDelphiString((NSStr as ILocalObject).GetObjectID);
end;

As explained by Darren Kosinski, every Cocoa wrapper type defined by the RTL implements ILocalObject, upon which you can call the GetObjectID method to get a reference to the underlying Objective-C object.

Anyhow, I’ve added the two issues to QC – check out reports 99308 (suboptimal definition of unichar in MacTypes.inc) and 99309 (parameters declared as unichar in Cocoa wrapper types when the should be UniCharPtr/PWideChar). [Update: and there’s a response! Only, the response is ‘need more info’ on both scores – apparently, the idea the Embarcadero guy might know the distinction between a character and an integer type in Pascal and Pascal-style languages is too much to ask, along with the thought he might try and actually use getCharacters in its current form before requesting a sample project on my part. Oh well!]

One thought on “Converting from a Cocoa string to a Delphi string

  1. Pingback: Malcolm Groves » Cross-platform Special Folders in FireMonkey

Leave a comment