Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

Porting a Large Project from Windows to Linux

4.84/5 (19 votes)
7 Jul 2022GPL314 min read 18K   217  
Analogous functions, subtle differences, multiple compilers...
How a large project was ported from Windows to Linux is discussed. The work involved implementing Linux targets for a platform abstraction layer, adopting C++ STL headers that did not exist when the project started, and switching to CMake to also perform builds with the gcc and clang compilers.

Introduction

To port software to a new platform, an abstraction layer that limits the use of platform-specific headers to a small number of .cpps is highly recommended. Parallel versions of those .cpps can then be developed to support the new platform without having to change the rest of the software. Implementing those .cpps involves finding analogs for the platform-specific functions of whichever platform is already supported. However, the abstraction layer may also have to evolve to handle subtle differences between the platforms. And the new platform may require the use of a new compiler, which may also mean having to use a new build tool.

Background

The code discussed in this article is taken from the Robust Services Core (RSC). If this is the first time that you're reading an article about RSC, please take a few minutes to read this preface.

Platform Abstraction Layer

RSC was developed on Windows, but the goal was to eventually support Linux as well. A platform abstraction layer was therefore defined from the outset so that Windows-specific headers would be used by a limited set of files. Windows targets were placed in *.win.cpp files that totalled about 3.5K lines of code, less than 1.5% of the project when the port finally began. The abstraction layer consisted of the following headers, for which the port would have to develop Linux targets:

Header Description
SysConsole.h console functions
SysFile.h directory functions
SysHeap.h subclass of Heap for the default C++ heap
SysLock.h low-overhead mutex
SysMemory.h memory allocation and protection
SysMutex.h mutex
SysSignals.h POSIX signals
SysThread.h threads
SysThreadStack.h stack traces
SysTickTimer.h high-resolution clock
SysTime.h time-of-day clock
SysIpL2Addr.h IP layer 2 address and functions
SysIpL3Addr.h IP layer 3 address and functions
SysSocket.h socket functions
SysTcpSocket.h TCP socket functions
SysUdpSocket.h UDP socket functions
RscLauncher.h automatic rebooting on failure

Linux targets were placed in *.linux.cpp files, with the IP networking targets being developed first. This was fairly straightforward because Windows and Linux are very similar when it comes to socket types and functions. But they report errors differently, which led to some refactoring that improved RSC's error reporting for sockets, an area that needed work in any case.

The effort then turned to kernel functions. When RSC originally began development, C++11 was not yet available. By the time the port started, compilers were implementing C++20. But it was C++11 that had added most of the headers that could be used for the port. Instead of implementing Linux targets, some of the platform abstraction layer could be eliminated and replaced with C++11 headers that already handled platform differences. To that end, these C++ library headers were adopted:

Header Description
<chrono> high-resolution and time-of-day clocks
<condition_variable> allows a thread to wait until it is notified or a timeout occurs
<filesystem> directory functions (C++17)
<mutex> mutexes
<ratio> used by <chrono>
<thread> threads

Adopting <chrono> also allowed RSC to remove Duration.h and TimePoint.h. Although both were platform independent, they supported time intervals and timestamps in a way that was incompatible with <chrono>.

Unlike <chrono>, <thread> eliminated very little code. It creates threads, but their stack sizes can't be specified. Its thread identifiers are opaque, and it features join, which is popular in toy examples but of limited use elsewhere. However, its sleep_for function proved useful. A major problem when creating a thread object is that the underlying thread often starts to run before its thread object has been fully constructed, because it has no idea that such an object is being created to wrap it. RSC has a feature-rich Thread class that includes a Pause function, but it can't be used until the Thread object has been constructed. After that, only Pause, never sleep_for, should be used. But sleep_for allows a thread to sleep for brief intervals, only proceeding once its Thread object has been fully constructed.

After adopting the new C++ library headers, the kernel portion of the platform adaptation layer looked like this:

Header Description
SysConsole.h retained
SysFile.h replaced by FileSystem.h, which uses <filesystem>
SysHeap.h retained
SysLock.h replaced by Lock.h, which used <mutex> (this header was later deleted)
SysMemory.h retained
SysMutex.h replaced by Mutex.h, which uses <mutex>
SysSignals.h retained
SysThread.h retained but partially replaced by Gate.h, which uses <condition_variable> and <mutex>
SysThreadStack.h renamed SysStackTrace.h, since C++23 will introduce <stacktrace>
SysTickTimer.h replaced by <chrono> and the simple SteadyTime.h
SysTime.h replaced by <chrono> and the simple SystemTime.h
RscLauncher.h retained

Static Analysis Tool

The first issue was that RSC has a C++ static analysis tool that is regularly used to clean up the code. But instead of just registering complaints, it provides an editor that allows two-thirds of them to be fixed interactively. This has become so useful that the tool must support whatever C++ language features RSC uses. And that's a problem because the tool is, in large part, a C++ compiler, and supporting all of the language features handled by a fully compliant C++ compiler isn't practical.

Adopting <chrono>, however, forced the tool to evolve. The problem wasn't so much <chrono> as its use of <ratio>, which is a rat's nest of template metaprogramming that converts one ratio to another at compile time.

ratio uses integer literals as template arguments, something that the tool didn't support. The tool wanted a template argument to be a type. It represents a type with TypeSpec, an abstract class with derived classes DataSpec (almost all types) and FuncSpec (a function type). A template name is represented by TypeName, which had a vector of TypeSpec for the template arguments that follow. Literals, however, are on another branch of the class hierarchy, below CxxToken. A TemplateArg class was therefore created. It wraps either a TypeSpec or an Expression (which can be an integer literal), and almost all of its functions simply forward to whichever of these two classes it is wrapping.

Next, the tool had to know that one instantiation of std::ratio was convertible to another. It already supported type conversions, and conversion constructors and operators. But none of these apply to two different ratios, which are two distinct template instantiations. To solve this, StackArg::MatchWith was modified to strip the template arguments from a std::ratio instantiation to detect convertibility.

Finally, the Linux target for POSIX signal handling needed to use sigaction to register its signal handler. This provides the handler with additional information about a signal so that, to simplify debugging, it can map SIGSEGV to RSC's proprietary SIGWRITE when faulty code writes to a valid address that is write-protected. However, some special needs developer had also used sigaction to name a struct that is passed to the function sigaction. This meant that the tool had to support elaborated type specifiers: prefixing the keyword class, struct, union, or enum to a type to distinguish it from something else with the same name, such as a function. Supporting this was fairly easy: Parser::GetTypeSpec was modified to look for one of those keywords before the type name.

Evolving the tool also uncovered some bugs and bizarre behaviors that had to be rectified. In the end, about as much time was spent enhancing the tool as was spent actually porting to Linux.

Differences Between Windows and Linux

During the port, various subtle differences between Linux and Windows had to be resolved.

Text files. If a text file created on Windows is read on Linux, problems occur. Windows uses \r\n (CRLF) at the end of a line, whereas Linux simply uses \n (LF). The function FileSystem::GetLine, with the same signature as std::getline, was therefore added to remove the \r, which would otherwise appear at the end of every string returned by std::getline when Linux was reading a file created on Windows.

Directory separator. Linux uses / to separate the directories in a path, but Windows uses \. The constant PATH_SEPARATOR was therefore initialized using std::filesystem::path::preferred_separator, which knows the platform on which it is running.

Heaps. Windows lets you create additional heaps with the same capabilities as the default C++ heap. Linux, however, doesn't support additional heaps. RSC was using Windows' heap interface to create additional heaps, but it had also developed its own BuddyHeap to support write-protected memory, because a Windows heap crashes if write-protected. Porting to Linux therefore meant using BuddyHeap for all of the additional heaps. However, the size of a BuddyHeap is fixed and can only be expanded by a restart, which was unacceptable for one of the heaps. RSC therefore had to implement the expandable SlabHeap to complement BuddyHeap.

POSIX signals. The C++ standard defines SIGABRT, SIGFPE, SIGILL, SIGINT, SIGSEGV, and SIGTERM. Windows adds SIGBREAK, which is similar to SIGINT, but Linux adds SIGBUS, which is similar to SIGSEGV.

Gate.h. This provides an object on which a thread waits until it is either notified or a timeout occurs. It replaced Windows' CreateEvent, WaitForSingleObject, and SetEvent, whose platform-independent analogs are Gate::Gate (constructor), Gate::WaitFor, and Gate::Notify. Although Gate doesn't contain much code, getting it right involved a fair bit of investigation, as well as fixes for some subtle bugs. Remarkably, Gate needs three critical-region objects to implement its interface: a condition_variable, a mutex, and an atomic_bool.

Thread identifiers. When a thread exits, RSC does not remove its identifiers (both native and RSC) from its ThreadRegistry. It only marks them Deleted, which allows debug tools to still find the native identifier and map it to RSC's identifier when displaying trace records that were captured before the thread exited. This worked well on Windows, but not on Linux. When a thread exits, the POSIX threads implementation on Linux immediately reassigns its identifier to the next thread that it creates. Because that identifier was still in the ThreadRegistry, the new thread blocked, forever waiting for its Thread object to be marked Constructed. The code had to be changed to handle this scenario.

Thread priorities. Although POSIX threads support priorities, the Linux scheduler doesn't support them in a standard build. This was a concern because, although RSC runs most threads at the same priority, it has two threads that run at higher priorities to mimic sanity and clock interrupts. It turned out that both of those threads could run at the same priority as all others and still do their jobs. This is because both Windows and Linux perform context switching at a rate that can only be described as vigorous, and so the would-be high-priority threads are scheduled frequently.

Stack traces. Although it was easy to capture a thread's stack on Linux, function names looked like they had been affected by transmission line noise when displayed. To remain compatible with C, a C++ compiler mangles function names when it adds them to an object file that includes debug information. The Windows function that maps an address to a function name demangles the name, but the equivalent Linux function does not. A separate utility must be used to do it.

Socket unblocking. RSC supports restarts, which are a partial reboot of the system. During a restart, a thread exits unless its data needs to survive. However, this poses a problem for blocked threads. How will they unblock so that they can exit? There is no way to unblock a read from cin, so CinThread always survives. For an instance of UdpIoThread, which blocks on recvfrom, the approach on Windows was to delete the thread's socket, which immediately caused recvfrom to return with an error. On Linux, nothing happened: recvfrom continued to block the thread. Some people disparage Windows, but I find it to be a good operating system. In any case, a different solution now had to be found, which was to have a UdpIoThread send a message to its own socket to unblock its recvfrom.

Analogous Functions

If you're porting software from Windows to Linux or vice versa, you often have to find the equivalent of some function on the new platform. Searching on the current function's name and the new platform's name often returns a link to a site where your question has been answered. If that doesn't work, Windows functions are documented here, and Linux functions are documented here. At least Windows groups its functions by topic, unlike Linux. Looking at all of this, I became even more convinced that a large operating system should use an object-oriented language, which would help to organize its morass of functions.

The following tables list analogous Windows and Linux functions used in RSC's platform abstraction layer. First, the kernel functions:

Header Windows Linux
SysConsole.h SetConsoleTitle a bizarre escape sequence
SysHeap.h HeapAlloc
HeapSize
HeapFree
HeapValidate
malloc
malloc_usable_size
free
mcheck (not thread safe)
SysMemory.h VirtualAlloc
VirtualFree
VirtualLock
VirtualProtect
VirtualUnlock
mmap
munmap
mlock
mprotect
munlock
SysThread.h SetPriorityClass
_beginthreadex
CloseHandle
signal
SetThreadPriority
setpriority
pthread_create
no analog when exiting a thread
sigaction (preferable)
pthread_setschedprio
SysStackTrace.h RtlCaptureStackBackTrace
SymFromAddr and
SymGetLineFromAddr64
backtrace
backtrace_symbols and
__cxxabiv1::__cxa_demangle
RscLauncher.h CreateProcessA
WaitForSingleObject and
GetExitCodeProcess
posix_spawnp
waitpid
 

And the IP networking functions, which have the same, or very similar, names:

Header Windows Linux
SysIpL2Addr.h gethostname gethostname
SysIpL3Addr.h getaddrinfo
freeaddrinfo
getnameinfo
getaddrinfo
freeaddrinfo
getnameinfo
SysSocket.h socket
bind
getsockopt
setsockopt
ioctlsocket
closesocket
socket
bind
getsockopt
setsockopt
ioctl
close
SysTcpSocket.h connect
listen
accept
WSAPoll
getsockname
getpeername
recv
send
shutdown
connect
listen
accept
poll
getsockname
getpeername
recv
send
shutdown
SysUdpSocket.h recvfrom
sendto
recvfrom
sendto

Building with Multiple Compilers

When the port started, RSC was built using the .vcxproj files created through VS2022's Properties sheets. To support builds on multiple platforms, Visual Studio recommended adopting CMake, about which I knew nothing. It seemed to be a popular build system, was well documented, and there were lots of questions and answers about it on stackOverflow. However, all this information was fragmented, and the examples were usually simple. Nothing addressed how to migrate a large project from VS2022 builds to CMake builds.

Fortunately, a search turned up an open-source tool, CMakeConverter, which analyzes .vcxproj files to generate CMake files. Using this tool was straightforward, and it did a good job creating the files needed by CMake. But there was still some work to do. RSC had a properties file that most of its projects shared, and the tool didn't know how to handle this. But the files that it generated gave a good sense of how to structure a large project in CMake. And one of those files, GlobalSettingsInclude, was clearly intended to serve the same purpose as that shared properties file.

Once the overall CMake framework existed, it was possible to gradually modify its files after finding answers to things like how to set compiler and linker options. VS2022 had also evolved to be rather well integrated with CMake. No longer do you open VS2022 from a solution or project. You can delete those files and just open the folder containing the source code. VS2022 then analyzes its CMakeLists file to configure the solution. Clicking on Manage Configurations… opens a tab that allows you to add new build configurations (compiler plus platform combinations), which VS2022 saves in a JSON file.

It didn't take long to successfully perform a build. This was a surprise, because I loathe learning a tool that needs a lot of configuration, and the C++ build process is ridiculous to begin with. Another surprise was that x64 builds were much faster, because they now used Ninja instead of VS2022.

Builds could now use the gcc or clang compilers as well as MSVC. Both gcc and clang generated new compiler warnings. Some were resolved by changing the code, and others were suppressed by adding compiler options to CMake files. However, a problem occurred with clang. RSC has a set of tests to see if it survives when bad things happen. One test is to divide by zero. This test passed if compiled with MSVC or gcc, but clang entered an infinite loop that kept dividing by zero. After reading some Windows documentation, I added a call to _fpreset in RSC's structured exception handler for Windows, which fixed the problem.

To test on Linux, I settled on WSL2 and Ubuntu. When RSC is launched through the Debug menu, VS2022 creates a Linux Console Window tab. But VS2022 needs a bit of work in this area, because it doesn't forward all keyboard events to that window. I discovered that just hitting the enter key did not cause std::getline(cin, str) to return; enter had to be preceded by a space. Similarly, ctrl-C did nothing. To confirm that RSC wasn't the problem, those things had to be tested on an actual Ubuntu console.

Until recently, changing the build process for this port would have required far more effort. But VS2022 has evolved to the point where it supports Linux and other C++ compilers rather well. I originally thought that I would also end up with a degraded IDE experience, even if I managed to get everything to work. But the transition was far easier than anticipated, and I actually prefer using CMake.

History

  • 7th July, 2022: Expand the section about the static analysis tool
  • 4th July, 2022: Initial version

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)