Editor's Note: This is the third installment of a five-part series by Gigi Sayfan on creating cross-platform plugins in C++. Other installments are: Part 1, Part 2, Part 4, and Part 5.
This is the third article in a series of articles entitled "Building Your Own Plugin Framework" that is about developing cross-platform plugins in C++. The first article described the problem in detail, explored various solutions and introduced the plugin framework. The second article explored the architecture and design of a plugin-based systems based on the plugin framework, the lifecycle of a plugin and the internals of the generic plugin framework. This article covers cross-platform development, miscellaneous topics like platform services provided to plugins by the system, error handling, and design and implementation of a dual C/C++ object model.
Cross-platform development in C/C++ is hard. Really hard. There are data type differences, compiler differences and OS API differences. The key to cross-platform development is to encapsulate platform differences so your main application code can concentrate on your application's logic. If your application code is bogged down in platform-specific code and has lots of #ifdef OS_THIS and #ifdef OS_THAT, it's a sure sign you need some refactoring. A good practice is to completely isolate all platform-specific code into a separate library or set of libraries. The ideal is that if you need to support a totally new platform you will need to modify only the code of the platform support library.
Understand your Target Platforms
The first order of business when targeting multiple platforms is to understand and be aware of the differences. If you target 32-bit and 64-bit platforms, you need to understand the ramifications. If you target Windows, you need to be aware of ANSI/MBCS versus Unicode/Wide string. If you target a mobile device with a stripped down OS, you need to know what subset is available for you.
Use a Good Cross-platform Library
The next order of business is to pick a good cross-platform library. There are several good libraries. Most of them focus on UI. I chose to use the Apache Portable Runtime (APR) for the plugin framework. APR is the foundation of the Apache web server, the subversion server and a few other projects.
But APR might not be right for you. It is fully documented, but the documentation is not stellar. There isn't a big thriving community. There are no books and relatively a small number of projects use it. To top it off it's a C library and you might not like the naming conventions. However, it is very portable and robust (at least the parts used by Apache and Subversion) and you know it can be used to implement high-performance systems.
Consider writing a custom wrapper to your cross-platform library (just the parts you use). There are several benefits to this approach:
- You can modify the interface to match your needs exactly
- The naming conventions will match the rest of your code
- It will make it much easier to switch to a different library or even upgrade to a new version of the same library.
There are also a disadvantages:
- You have to invest time and resources in writing and maintaining your wrapper
- Debugging could be a little more difficult because you have to go through another layer (not to mention that your wrapper may be buggy).
I chose to write a wrapper for APR because it is a C library that require that you release resources explicitly, deal with memory pools for efficient memory allocation and I didn't like the naming conventions and the entire feel. I also used a relatively small subset (just directory and file APIs), but it was still a significant work to do it, test it and debug it. You can see the Path and Directory classes in the plugin_framework subdirectory. Note, that I use the APR typedefs for basic types as is without wrapping them in my own typedefs. This is just out of laziness and not a recommended practice. You can see the APR types all over the place in structs and interfaces.
Data Type Differences
The integral types C++ inherited from C are a cross-platform hazard. int, long and friends have different sizes on different platforms (32-bit and 64-bit on today's systems, maybe 128-bit later). For some applications it might seem irrelevant because they never approach the 32-bit limit (or rather 31-bit if you use unsigned integers), but if you serialize your objects on a 64-bit system and deserialize on a 32-bit system you might be unpleasantly surprised. Again, no easy breaks here. You need to understand the issues and do the right thing, which is make sure the sending/saving side and the receiving/loading side agree on the number of bytes of each value. File format or network protocol designers must be smart about it.
APR provides a set of typedefs for basic types that might be different on different platforms. These typedefs provide a guaranteed size and avoid the fuzzy built-in types. However, for some applications (mostly numerical) it is sometimes important to use the native machine word size (typically what int stands for) to achieve maximal performance.
Wrap Platform-specific Components In Cross-platform Wrappers
Sometimes you must write larger chunks of platform-specific code. For example, APR's support for dynamic libraries wasn't adequate for the plugin framework needs. I simply implemented the DynamicLibrary class, which is a cross-platform abstraction of the dynamic library concept with a neutral interface. It is not always possible to do it without losing expressiveness or performance. You need to make your own trade-offs and expose to the application what you must. In the case of DynamicLibrary I preferred a minimal interface that doesn't allow the application to specify any flags to dlopen() on Unix and I used the simpler LoadLibrary() on Windows in lieu of the more flexible LoadLibraryEx().
Organize All Third-party Dependencies
Today, it's common practice to reuse third-party code. There are many good libraries with liberal licenses. Your project is likely to use a few of them too. In cross-platform environment it is important to select the third-party libraries you use wisely. In addition to standard selection criteria like robustness, performance, ease of use, documentation and support, you need to pay attention to the way the library is developed and maintained. Some libraries have not been developed in a cross-platform fashion from the beginning. It is common to see libraries that are used mostly on one platform and ported as an afterthought to other platforms. If the code base is not unified, it is a red flag. If there is a small number of users on a particular platform it is a red flag. If the core developers or maintainers don't don't package the library for all platforms it is a red flag. If when new version comes out, some platforms lag behind it is a red flag. A red flag doesn't mean you shouldn't use the library. It just means that all other things being equal you should prefer a library without red flags.
Invest in the Build System
A good automated build system is crucial for the development of non-trivial software systems. If you throw in several flavors of your system (Standard, Pro, Enterprise), a couple of platforms (Windows, Linux, Mac OS X) and a few build variants (Debug, Release) you get an exponential explosion of artifacts. The build system must be fully automated and support the full build lifecycle -- get the source code from the source control system, do any pre-processing, compile, link, run unit tests and integration tests, package and distribute, possibly run full system tests, and report the results to all the stakeholders.
I'm a little fanatic about build systems and automation, but you won't be sorry for investing in your build system.