Building a multi-platform library using C++
CartoType, the mapping, routing and geocoding library I started writing in 2003, has a core library written in C++ making up over 95% of the code, and is delivered as SDKs for C++, iOS using Swift and Objective C, Android using Java, and .NET using C# and other languages. How do I achieve and manage that portability? This article briefly explains my approach.
Copyright notice
The copyright to this article and the code quoted in it is (C) CartoType Ltd, 2004-2021.
Choosing C++
There was never any doubt that I would use C++ for the CartoType project. When I founded CartoType in 2003 I had been using C++ professionally for about ten years and I was very comfortable with it. I was also free of any delusions about costs. C++ was designed on the principle of never paying for what you don't need; there is no unavoidable cost to C++ compared with C, and there are a lot of benefits. The main ones, which are completely cost-free and make programs much safer, are access control (private / public / protected), which allows you to make interfaces as small as possible - the smaller an interface, the less chance of error - and constructors and destructors, which enable the RAII idiom (resource acquisition is initialisation).
At first I had to use C++ in a very restricted way. I couldn't use floating point because it was far too slow on some platforms like Symbian, and I couldn't use exceptions because they weren't supported everywhere, and because of that I couldn't use the STL (standard template library). The story of how I got round those limitations is a subject for another article.
Nowadays, eighteen years later, the limitations don't exist. Floating-point works fast everywhere, and 'modern C++', a term comprising C++11 and later variants, is supported on all platforms for which one might want to do mapping and routing. In fact CartoType now uses C++14 and will soon move to C++17 and exceptions are used throughout, as is the STL, now part of the standard C++ library. As a further advantage, modern C++ provides library support for threads and concurrency, which CartoType makes great use of in its hardware-accelerated graphics system.
Graphics using OpenGL ES 2.0
A map rendering library must draw maps smoothly, animating all transitions when panning, rotating or zooming. I chose OpenGL ES 2.0 for CartoType's graphics system because it is supported by all the various platforms. There is one looming difficulty: Apple has deprecated OpenGL and will one day stop supporting it. It is still supported in iOS 14, and CartoType will soon move to MetalAngle, an open-source OpenGL ES layer on Apple's Metal graphics system.
Writing a .NET wrapper using C++/CLI
There's only one sensible way to write a .NET wrapper for C++ code, and that is to use the 'bridge language' C++/CLI. You create a series of peer classes using the .NET language system, and write functions in those classes to call functions in the C++ API. Each peer class will typically own a C++-class object, or in the case of simple structures, have the same members. Build the C++/CLI wrapper using Microsoft Visual C++.
C++/CLI is an extended C++ with extra keywords allowing you to create classes (ref class) and enumerated constants (enum class) compatible with the .NET common language system, and to allocate objects on the managed heap (use gcnew). When you compile the code, you get a DLL that can be linked into a C# or VB.NET program. The way it works for CartoType is that there are a source file and header making up the .NET wrapper: CartoTypeWrapper.cpp and CartoTypeWrapper.h. This source is compiled in C++/CLI and linked with the CartoType library compiled in C++.
Here's an example of how a typical CartoType API function is exposed to .NET. The function in the C++ API is
TResult CFramework::LoadFont(const CString& aFontFileName)
It's a member of the main class used to manage the map and routing state, CFramework, and it loads a font (usually a TrueType font) from a file. It takes the file path as a string in CartoType's own UTF-16 string class and returns an integer result code. (Yes, I'm aware that it's not necessarily a good idea for a library to have its own string class; it's there for historical reasons. You can construct a CString implicitly from a char* or a std::string, so in practice there is very little impedance mismatch.)
The .NET wrapper has a peer class to CFramework, called Framework, which owns a C++ CFramework object via a member called m_framework. The wrapper function for LoadFont has the same name and looks like this. It naturally uses the built-in .NET string class:
Result Framework::LoadFont(String^ aFontFileName)
{
StringArg(aFontFileName);
return (Result)(int)m_framework->LoadFont(aFontFileNameText);
}
StringArg needs to be explained. It's a macro to create a CartoType string called <string>Text from a .NET string called <string>, and is
#define StringArg(X) CString X##Text; SetString(X##Text,X)
and SetString is:
static void SetString(MString& aDest,String^ aSource)
{
if (aSource == nullptr || aSource->Length == 0)
{
aDest.Clear();
return;
}
cli::array<wchar_t,1>^ a = aSource->ToCharArray();
pin_ptr<const wchar_t> p = &a[0];
TText source((uint16_t*)p,aSource->Length);
aDest.Set(source);
}
which demonstrates the use of pin_ptr to get a pointer to the memory of a managed array.
It's beyond the scope of this article to explain C++/CLI in detail, but there are a few further points worth touching on:
References to managed class objects in C++/CLI use the ^ sign, as in String^. Users of C#, VB.NET, etc., can just use references as normal. For example, here's some code including a call to the .NET LoadFont function in our simple C# demo program:
string font_path = Application.StartupPath + "/../../../../../font/";
if (!System.IO.File.Exists(map_file)) // if we're running in an ordinary source tree, not an SDK
{
map_file = Application.StartupPath + "/../../../../../../../map/isle_of_wight.ctm1";
style_file = Application.StartupPath + "/../../../../../../../style/standard.ctstyle";
font_path = Application.StartupPath + "/../../../../../../../font/";
}
m_framework = new CartoType.Framework(map_file,
style_file,
font_path + "DejaVuSans.ttf",
this.ClientSize.Width,
this.ClientSize.Height);
m_framework.LoadFont(font_path + "DejaVuSans-Bold.ttf");
ref classes that own objects on the unmanaged heap will need a finalizer: a sort of destructor called by the garbage collector; e.g.; the CartoType C++/CLI class MapObject owns a C++ CMapObject, and has a finalizer called !MapObject():
MapObject::!MapObject()
{
delete m_map_object;
}
You can't use std::unique_ptr in a managed class (a 'ref class'), so m_map_object is of type CMapObject*, not std::unique_ptr<CMapObject>.
Use the % operator to pass by reference when you need a function to return multiple values. The & operator dereferences an ordinary pointer (*), while the % operator dereferences a managed pointer (^). For example, here's the CreateRoute function, which needs to return a result code as well as a route:
Route^ Framework::CreateRoute(Result% aResult,RouteProfile^ aProfile,RouteCoordSet^ aCoordSet)
You call it from C# using the 'ref' keyword before the aResult parameter.
Building the .NET SDK
I build the CartoType .NET SDK using a batch file which calls msbuild. It extracts the latest sources from the repository, builds the C++ library and the .NET wrapper and links them together, builds various tools, and puts everything into a zip file labelled with the current version number.
Writing an Android wrapper using Java and the JNI (Java Native Interface)
An Android wrapper is created in two parts:
The Java part consists of Java classes that are peers of corresponding classes in the C++ library. A Java class object has to either mirror the C++ object by having the same data members, or it has to own a C++ object. The latter can be accomplished by means of a private long integer data member that is actually a pointer to the C++ object. For example, CartoType's Geometry class, which is a peer of CGeometry in C++, is declared like this (omitting public members):
public class Geometry implements Path
{
public Geometry(CoordType aCoordType)
{
construct(aCoordType.ordinal(),null,null);
}
public native void beginContour();
// ... many public members omitted ...
// constructor used by JNI to create a Geometry object
Geometry(long aGeometryRef)
{
iGeometryRef = aGeometryRef;
}
// called by the Java garbage collector: calls native function which
// deletes iGeometryRef
protected void finalize()
{
destroy();
}
private native void construct(int aCoordType,Rect aRect,MapObject aMapObject);
private native void destroy();
// this is actually a pointer to the C++ CGeometry class
long iGeometryRef;
// runs C++ code to set up a global field ID for iGeometryRef
private static native void initGlobals();
// called when the class is loaded
static
{
System.loadLibrary("cartotype");
initGlobals();
}
}
The JNI part consists of C++ functions declared in Java using the native keyword, and in C++ having names incorporating the Java class and function they represent. For example, the method Geometry.beginContour() declared as 'public native void beginContour()' is implemented in the JNI code as:
extern "C" void Java_com_cartotype_Geometry_beginContour(JNIEnv* aEnv,jobject aObject)
{
CGeometry* g = (CGeometry*)aEnv->GetLongField(aObject,TheGeometryRefFieldId);
g->BeginContour();
}
Although the Java method has no arguments, the JNI function has two: a pointer to the Java environment, and a pointer to the object on which the method is called (the this pointer). The function JNIEnv::GetLongField gets the value of iGeometryRef, which is then cast to CGeometry. The C++ function can then be called.
On-line documentation for the JNI is rather poor. You will need the book The Java Native Interface by Sheng Liang (Addison-Wesley, 1999). It's a couple of decades old but it has stood the test of time.
Some useful JNI features:
You can create Java objects in C++ code. That makes it easy to write functions that return Java strings or arrays. You can even create objects of your own classes. Here's some code that creates a CartoType Java Route object. It passes the method ID of the Java class's constructor to NewObject.
jmethodID route_constructor = aEnv->GetMethodID(TheRouteClass,"<init>","(J)V"); jobject route_object = aEnv->NewObject(TheRouteClass,route_constructor,(jlong)route_ptr);
You can access the content of arrays, both for reading and writing.
You can write log messages to the Android debugging console using the __android_log_print and __android_log_write macros.
Building the Android SDK
I build CartoType's Android SDK using an Android Studio library project, the output of which is an .AAR file that can be dropped into an Android app as a module. The C++ code is built as a library containing all the core code plus the JNI source files, using the Android NDK, and made part of the Android Studio project using the 'external native build' mechanism.
Writing an iOS wrapper using Objective C
An iOS API needs to be provided in both Swift and Objective C. Swift code cannot call C++ code directly, but Objective C, in the form of its variant Objective C++, can do so, and it's possible to use Objective C functions from Swift using a mostly automatic bridging system.
The Objective C wrapper consists of
- Header files containing declarations of Objective C class interfaces, ordinary C structures, and C-style enumerated constants. These files have the extension .h.
- Implementation files containing Objective C++ code. These files have the extension .mm. They contain Objective C methods which call the C++ functions.
The header files must contain no C++ code: just Objective C. That allows them to be used in both Objective C and Objective C++ applications.
Objective C can use three different systems to embody the peer relationship with the C++ class:
1. A struct for small classes with independent members and no complex state or invariants:
/** An axis-aligned rectangle. */
typedef struct
{
/** The minimum X coordinate: normally the left edge. */
double x_min;
/** The minimum Y coordinate: the top if coordinates increase downwards, otherwise the bottom. */
double y_min;
/** The maximum X coordinate: normally the right edge. */
double x_max;
/** The maximum Y coordinate: the bottom if coordinates increase downwards, otherwise the top. */
double y_max;
} CartoTypeRect;
In the implementation code the members are copied back and forth as needed between this struct and the native C++ class CartoType::TRect.
2. An Objective C class with parallel members to the C++ peer class. Here is the declaration in CartoTypeBase.h:
/** Information returned by the getMatch method of CartoTypeMapObject. */ @interface CartoTypeMatch: NSObject /** The name of the attribute in which the matched text was found. */ @property (nonatomic, strong) NSString* key; /** The value of the attribute in which the matched text was found. */ @property (nonatomic, strong) NSString* value; /** The start position of the matched text within the value. */ @property (nonatomic) size_t start; /** The end position of the matched text within the value. */ @property (nonatomic) size_t end; @end
and here is the implementation in CartoTypeBase.mm:
@implementation CartoTypeMatch
{
NSString* m_key;
NSString* m_value;
size_t m_start;
size_t m_end;
}
@synthesize key = m_key;
@synthesize value = m_value;
@synthesize start = m_start;
@synthesize end = m_end;
@end
The whole implementation, including the data members m_key, m_value, m_start and m_end, is made private by placing it in the .mm file. The data member values are automatically zero-initialized when a CartoTypeMatch object is created in Objective C.
3. An Objective C class that owns an object of the C++ peer class. Here is the declaration of CartoTypeMapObject in CartoTypeBase.h. It is the peer of the C++ class CMapObject.
/** A map object: a point, line, polygon or array (texture), with its name, layer, ID and other attributes. */
@interface CartoTypeMapObject: NSObject <CartoTypePath>
/** Initializes a CartoTypeMapObject with a C++ map object; for internal use only. */
-(id)initWithMapObject:(void*)aMapObject;
/** Destroys the map object. */
-(void)dealloc;
/** Gets the type of the map object: point, line, polygon or array. */
-(CartoTypeMapObjectType)getType;
// ... many public methods omitted ...
/** Gets a pointer to the C++ map object; for internal use only. */
-(void*)getObject;
@end
Here is the implementation in CartoTypeBase.mm:
@implementation CartoTypeMapObject
{
CMapObject* iMapObject;
}
-(id)initWithMapObject:(void*)aMapObject
{
if (self = [super init])
{
iMapObject = (CMapObject*)aMapObject;
return self;
}
return nil;
}
-(void)dealloc
{
delete iMapObject;
}
-(CartoTypeMapObjectType)getType
{
return (CartoTypeMapObjectType)iMapObject->Type();
}
// ... many method implementations omitted ...
-(void*)getObject
{
return iMapObject;
}
@end
It's worth emphasizing that the whole power of modern C++ and the C++ library can be used in the implementation of Objective C class methods. CartoType's iOS wrapper uses aggregate initialisation, range for, lambda functions and STL aggregate classes, freely intermixed with Objective C constructs.
Building the iOS SDK
I develop for iOS in Xcode. The iOS SDK is built by a bash script running on my Macintosh. It extracts the source code from the repository, calls xcodebuild on the Xcode project file to make the iOS library and some tools including makemap, the map creation utility, then packages them up as a .DMG file (an Apple Disk Image) labelled with the version number.
Source control and issue tracking
Although it's not really part of the subject of this article. it may be of interest to know that CartoType's source code is kept in a series of Mercurial repositories managed by the FogBugz Kiln system, which integrates tightly with their issue tracker.
Publicly available source code, mostly that of demonstration apps, is kept in GitHub.