Due to differences in operating systems and differences in the versions of the same operating system, the functions provided by the C ++ standard library are still limited and the C ++ compiler products are not fully compatible, this makes it difficult to solve many problems when porting large applications. How can we reasonably avoid them improving the portability of C ++ programs, the author puts forward some practical suggestions from the aspects of source code organization and arrangement.
When writing server-side software products, we often need to release a variety of different platform versions for the same software product. This is because no server operating system can control the entire world. There are many servers running Windows, but there are also many servers running Linux and Various UNIX operating systems, and there are slight differences between various UNIX operating systems. In addition, in some large enterprises (especially large banks), the servers that run key businesses are often IBM's mainframes, and their operating systems are somewhat different from general UNIX.
In addition, software-dependent middleware, called function libraries, and required compilers can all be considered part of the platform. Any combination of the above content may cause a lot of possibilities. If the platform portability is poor, the software may run normally in your development environment, but various strange problems may occur in the customer's environment.
Maybe you will say that none of these is a problem. Isn't everything okay if you use Java to write programs? Unfortunately, sometimes some legacy code is written in C, or a key function library that you must rely on only provides C APIs. After evaluation, it is found that the code is rewritten in Java, or it takes too much work to encapsulate the legacy code or CAPI through JNI and other possible cross-language calling mechanisms. At this time, C ++ is often a more appropriate choice.
One major reason for using Java to write programs across platforms is that Java has an all-encompassing standard library, while the C ++ Standard Library only provides the most basic functions. Using C ++ to write a large program almost always calls APIs outside the standard library. These APIs may not be cross-platform. Therefore, the first point to note when writing C ++ programs that are easy to transplant is: if you have a choice, use cross-platform APIs as much as possible.
For example, for file operations, the Windows 32 API and the file operation functions provided by the UNIX operating system are different. Which one should I choose? Are not suitable, it is best to rely on the standard library, fstream or fopen/fclose can be. To create and synchronize threads, the Windows 32 API and UNIX operations are different. Is there any cross-platform solution? Yes, pthreads is cross-platform. If your system needs to perform operations on strings, are you using the CString provided by MFC or the string in the standard library? The latter should be selected because MFC is not cross-platform.
So what if some of the APIS you have to use do not have cross-platform implementation, but are implemented by the platform itself? For example, on Windows, LoadLibrary is used to load dynamic libraries, and dlopen is used to load dynamic libraries on UNIX. There seems to be no cross-platform implementation. So what should we do? Can I write this in every place where the dynamic library is to be loaded?
# Ifdef WIN32
HMODULE h = LoadLibrary ("libraryname ");
# Elif defined (UNIX)
Int h = dlopen ("libraryname", RTLD_LAZY );
# Endif
This is what many software do. However, this is terrible because the platform-related code is mixed with the independent code of other platforms, and the code will spread a lot # ifdef, which affects reading; if you need to port the code to another platform later, you may need to modify the location where each dynamic library is loaded and add a # elif defined (?), The workload will be large.
We recommend that you encapsulate a cross-platform implementation by yourself. In the independent code of the platform, you can only call this cross-platform API to isolate the platform relevance. Of course, this layer of encapsulation should be very thin. You only need to use one or two rows of inline functions and several typedef functions. In this way, the guiding ideology is to add indirect layers through encapsulation to separate the independent platform code from the related platform code.
Let's take a look at it. Can this be done?
In main. cpp (assuming we need to load the dynamic library in this file), write as follows:
# Include "platform_specific.hpp"
Int main (){
Handle_t h = MyLoadLibrary ("libraryname ");
// Use the dynamic library and then uninstall it.
}
In platform_specific.hpp, write as follows:
# Ifdef WIN32
Typedef HMODULE/* WIN32 handle type */handle_t;
Inline handle_t MyLoadLibrary (const string & libname)
{
Return LoadLibrary (libname. c_str ());
}
# Elif defined (UNIX)
Typedef int/* UNIX handle type */handle_t;
Inline handle_t MyLoadLibrary (const string & libname)
{
Return dlopen (libname. c_str (), RTLD_LAZY)
}
# Endif
In this way, the independent code of the platform is separated from the code of the platform. In main. cpp, the independent code of the platform is separated. In platform_specific.hpp, the code of the platform is separated. When porting to a new platform, you do not need to make any changes to main. cpp. You only need to modify the implementation of MyLoadLibrary in platform_specific.hpp, and you only need to change this one. However, platform_specific.hpp becomes messy and full of # ifdef. Imagine that apart from MyOpenLibrary, there may also be MyCloseLibrary, MyBindSymbol, and so on, all the cross-platform APIs encapsulated by myself (that is, the APIs that need to be written in the implementation # ifdef (a certain OS) all in it. This file will become difficult to maintain, and it is likely that many people are maintaining it (each person is responsible for a different platform), and the changes will be very frequent (especially if versions of several platforms are developed simultaneously ). Is there any better way?
In platform_specific.hpp, just put the following content:
# Ifdef WIN32
# Include "win32_specific.hpp"
# Endif
# Ifdef UNIX
# Include "unix_spefic.hpp"
# Endif
The platform-related implementation is included in the header files of each platform. For example, win32_speific.hpp is like this:
Typedef HMODULE/* WIN32 handle type */handle_t;
Inline handle_t MyLoadLibrary (const string & libname)
{
Return LoadLibrary (libname. c_str ());
}
In unix_specific.hpp:
Typedef int/* UNIX handle type */handle_t;
Inline handle_t MyLoadLibrary (const string & libname)
{
Return dlopen (libname. c_str (), RTLD_LAZY )\
}
This greatly reduces the number of # I f d e f. # Ifdef (several platforms are supported) will appear in platform_specific.hpp, and is not required in all other files. In addition, the focus is also separated: persons responsible for implementing independent platform functions focus on writing and maintaining main. cpp, and the person responsible for porting to each platform writes and maintains the OS _specific.hpp of their respective platforms. It does not cause conflicts between multiple users who modify the same file. The independent code of the platform and the related code of the platform are also well separated.
There are two points worth noting:
First, # elif is not used in platform_specific.hpp, but an independent # ifdef # endif block is used. The purpose is to support the following topology:
# Ifdef WIN32
# Include "win32_specific.hpp"
# Endif
# Ifdef WINCE
# Include "wince_specific.hpp"
# Endif
# Ifdef UNIX
# Include "unix_spefic.hpp"
# Endif
# Ifdef SOLARIS
# Include "solaris_specific.hpp"
# Endif
# Ifdef AIX
# Include "aix_specific.hpp"
# Endif
WIN32 does not conflict with WINCE. WINCE is a special WIN32. Solaris and AIX are two special UNIX types, and they do not conflict with UNIX. If # elif is used, # include cannot be used at the same time, but the above topology structure can be used, and the same things on UNIX platforms can be implemented in unix_specific.hpp, the difference between Solaris and AIX is implemented in solaris_specific.hpp and aix_specific.hpp to further segment the platform.
Second, win32_specific.hpp and unix_specific.hpp can only be used to encapsulate platform-related APIs and cannot contain too many independent platform logic.
The following is an example:
In unix_specific.hpp:
Int main ()
{
// Do platform-independent tasks
Int h = dlopen ("library", RTLD_LAZY );
// Continue to do platform-independent tasks
}
In win32_specific.hpp:
Int main ()
{
// Do platform-independent tasks
HMODULE h = LoadLibrary ("library ");
// Continue to do platform-independent tasks
}
This is not good. Some of the platform-independent code will be copied and pasted and repeated in two places. Copy and paste are the taboos of programming. Therefore, you must note that the encapsulated functions can only be very simple inline functions with only one or two lines, and there is no platform-independent code.
Using this source file topology can greatly improve the software portability, and it brings little trouble to write the first platform version. If your development strategy is synchronous development on different platforms, this will allow developers on different platforms and cross-platform modules to work in different source code files without conflict; if your development strategy is to first release a version of a platform and then transplant it to another platform, using this source code structure can also bring you great benefits: assume that the first version is Windows and the Linux version will be released later.
Cpp (which represents the independent code of all platforms) and win32_specific.hpp. Compile a linux_specific.hpp according to the implementation of win32_specific.hpp.
It is easy to maintain. In the future, you only need to work on a code tree to release an upgrade version or patch/servicepack, without worrying about merging and modifying branches. In addition, if a bug only appears on a platform but does not exist on other platforms, you only need to look at the bug in OS _specific.hpp corresponding to that platform, this is the benefit of separating focus.
As I mentioned earlier, a platform can refer to a wider range of concepts, such as middleware or a third-party library on which you depend, in addition to the operating system. This method can be applied as long as your dependence on the platform is local rather than global (for example, the dependency on the Framework. Here I chose to use # ifdef and # include to selectively include and compile platform-related code. This is the best and easiest way to do this. Both C and C ++ support the compiler on all platforms. Of course, there are other methods, such as using namespace definitions, using namespace import statements, and template instantiation (using the operating system type as a template parameter. The pre-compiler and # are terrible.
A friend may wish to try.
Such a file structure can also be used for makefile. During compilation, make-e OS = YOURTARGETOS [other parameters] are used to selectively build a platform. Makefile should contain the following content:
Include $ (ROOT)/buildenv/default. inc # platform independent build information
Include $ (ROOT)/buildenv/$ (OS). inc # platform-related building information, such as different platforms
# Parameter definitions of different Compilers
Because the macro definition with the inclusion order can overwrite the previous one, default. inc can also provide default values for compilers on various platforms (for example, define the default value of the compiler as cc, and some platforms can overwrite the default value as gcc or xlC. in inc, the default value is-O3, which supports platforms with higher optimizations. -O5 in inc, and so on ). In addition to overwriting, macros can also be connected. The length of makefile writing is not detailed here. In fact, there are automatic tools (such as autoconf, autoheader, and automake) that are used together with GNU make to generate platform-related files and build platforms (for specific usage, refer to Google to find documents ), however, in many cases, it is not necessary to use the whole structure to kill chickens, but there are still many details to be aware. For example, if the file path separator "/" and "\" are different (boost: path well encapsulates this difference), is the file system of this operating system case sensitive, differences between Big Endian and Little Endian: Differences in word lengths on different platforms, and differences in default alignment of different platforms/compilers. In addition, note that some APIs provided by the C ++ compiler actually extend the ANSI or ISO standards, such as hash_map, hash_set, and rope in sgi stl, there are also some functions such as snprintf provided by the C library. These APIs are not cross-platform and should be avoided (for example, the C library on S/390 does not contain the snprintf function, most STL implementations do not have hash_map, hash_set, and rope ). However, if you think that using them will bring great convenience, you can also use it, but you have to implement snprintf, hash_map, and rope on OS _specific.hpp, which does not support these Apis. Due to space limitations, we will not discuss these details.
Finally, it is important to mention that the software should have sound logical and physical design as much as possible. Porting to a different platform is essentially modifying the software. The better the design, the easier the software to modify. Poor design will lead to unclear Software Logic and code tangle, and a little change will lead to the whole body. Such software is difficult to transplant. The locally modified software does not affect the rest, and only one change needs to be done once. You do not need to perform a global search and replace the software. You are also worried that the missing one will cause a bug, such software can be easily transplanted.
Bytes ----------------------------------------------------------------------------------------
Key points for compiling C/C ++ programs that can be transplanted
1. Hierarchical Design to isolate the Code related to the platform. Just like testability, portability also needs to begin with design. In general, the top and bottom layers do not have good portability. The top layer is GUI, and most guis are not cross-platform, such as Win32 SDK and MFC. Operating system...
1. Hierarchical Design to isolate the Code related to the platform. Just like testability, portability also needs to begin with design. In general, the top and bottom layers do not have good portability. The top layer is GUI, and most guis are not cross-platform, such as Win32 SDK and MFC. The lowest layer is the operating system API. Most operating system APIs are dedicated.
If the two layers of code are distributed across the entire software, the software's portability will be very poor, which is self-evident. So how can we avoid this situation? Of course, it is a hierarchical design:
The underlying layer uses the Adapter mode to encapsulate APIs of different operating systems into a set of unified interfaces. As for whether to encapsulate the program into a class or a function, it depends on whether you use the C program or the program written in C ++. This seems simple, but it doesn't seem as simple as it is (you will understand it after reading the entire article). It will take you a lot of time to write code and test them. It is wise to use existing libraries. There are many such libraries. For example, the C library has glib (the GNOME basic class), and the C ++ library has ACE (ADAPTIVE CommunicationEnvironment, using these libraries when developing the first platform can greatly reduce the workload of porting.
The top layer adopts the MVC model to separate the interface performance from the internal logic code. Put most of the Code into the internal logic. The interface only displays and receives input. Even if you want to change the GUI, the workload is not great. This is also one of the ways to improve testability. Of course there are other additional benefits. Therefore, even if you use a cross-platform GUI such as QT or GTK + to design the software interface, it is very useful to separate the interface performance and internal logic.
If the above two points are achieved, the portability of the program is basically guaranteed, and the other issues are only technical details.
2. Familiarize yourself with the target platforms in advance and reasonably abstract the underlying functions. This is based on the layered design. Most underlying functions, such as threads, synchronization mechanisms, and IPC Mechanisms, correspond to almost one-to-one functions provided by different platforms, encapsulating these functions is very simple, and the work of implementing the Adapter is almost just physical activity. However, for some special applications, the form component itself, take GTK + for example. The XWindow-based and Win32-based functions are very different, in addition to basic concepts such as windows and events, there is almost no difference. If you do not know the features of each platform in advance, you should carefully consider them during design, abstract pumping ports are almost impossible on another platform.
3. Try to use standard C/C ++ functions. Most platforms implement functions specified by POSIX (Portable Operating SystemInterface), but these functions may perform more performance than Native functions, it is not as convenient as native functions. However, it is best not to use native function functions because they are cheap. Otherwise, the stone will eventually reach its own feet. For example, functions such as fopen are used for file operations, rather than functions such as CreateFile.
4. Do not use the features in the C/C ++ standard. Not all compilers support these features. For example, VC does not support the macros of variable parameters required in C99, and VC does not fully support some template features. For the sake of security, do not be too radical.
5. Do not use features not explicitly defined in the C/C ++ standard. For example, if you have multiple dynamic databases, each of which has a global object, and the construction of these global objects has dependencies, you will have trouble sooner or later, the order in which these global objects are constructed is not specified in the standard. The operation on one platform is correct. On another platform, it may not be clear that the program will crash, and a lot of modifications will be made to the program.
6. Try not to use quasi-standard functions. Some functions are widely used on most platforms, so that everyone regards them as standards, such as atoi (converting strings into integers) and strdup (cloning strings) alloca (allocate automatic memory on the stack) and so on. Don't be afraid of 10 thousand, just in case, unless you understand what you are doing, it is better not to touch them.
7. Pay attention to the details of standard functions. Maybe you don't believe that even a standard function, regardless of internal implementation, is sometimes surprising about its external performance differences. Here are a few examples:
Int accept (int s, struct sockaddr * addr, socklen_t * addrlen); addr/addrlen is originally an output parameter. If it is a C ++ programmer, no matter what, you are used to initializing all the variables. If it is a C programmer, it is hard to say. If they are not initialized, the program may be inexplicably crash, and you may not dream about it. This is okay under Win32 and will only happen in Linux.
Int snprintf (char * str, size_t size, const char * format ,......); The second parameter, size, does not include null characters in Win32, and contains null characters in Linux. the difference may take several hours.
Int stat (const char * file_name, struct stat * buf); this function is no problem in itself. The problem lies in the structure stat. st_ctime represents the create time under Win32, in Linux, the last modification time is represented.
FILE * fopen (const char * path, const char * mode); there is no problem in reading binary files. Be careful when reading text files. Automatically pre-process the files under Win32. The read content is different from the actual length of the file. in Linux, there is no problem.
8. Be careful with the standard data type. Many people have suffered from the transformation of the int type from 16-bit to 32-bit. This is a long time. Do you know that char is signed on some systems and unsigned On some systems? Do you know that wchar_t is 16 bits in Win32 and 32 bits in Linux? Do you know that there is a signed 1bit bit field. The values are 0 and-1 instead of 0 and 1? These beautiful things, the end of which is a ghost, accidentally with its path.
9. It is best not to use the unique features of the platform. For example, a DLL in Win32 can provide a DllMain function. at a specific time, the Loader of the operating system automatically calls this function. This type of function is very useful, but it is best not to use it. This type of function cannot be guaranteed on the target platform.
10. It is best not to use the unique features of the compiler. Modern compilers are very user-friendly and considerate, and some functions are very convenient to use. For example, in VC, if you want to implement local thread storage, you do not call functions such as TlsGetValue/Tls TlsSetValue. Just add _ declspec (thread) in front of the variable, however, although pthread has similar functions, it cannot be implemented in this way, so it cannot be transplanted to Linux. Gcc also has many extensions, which are not available in VC or other compilers.
11. Pay attention to the features of the platform. For example:
In the DLL under Win32, other functions are invisible to the outside unless explicitly specified as the export function. In Linux, all non-static global variables and functions are externally visible. This should be especially careful. The problems caused by functions of the same name make it difficult for you to check for two days.
Directory separator. '\' is used in Win32 and '/' is used in Linux '/'.
Text File line breaks: Use '\ r \ n' in Win32,' \ n' in Linux, and '\ R' in MacOS '.
The byte sequence (large-end/small-end) may be different for different hardware platforms.
Bytes alignment. On some platforms (such as x86), bytes are not alignment, which is nothing more than a slower speed. On some platforms (such as arm), it completely reads data in the wrong way, you will not be prompted. If something goes wrong, you may not have a clue at all.
12. It is best to know the resource limits of different platforms. You must remember that the number of files simultaneously opened in DOS is limited to dozens. Today, the operating system is much more powerful, but not unlimited. For example, the default maximum shared memory in Linux is 4 MB. If you limit the common resources on the target platform, it may be of great help and some problems can be easily located.
Author's "path to growth"