Back to TOCFeatures


Cross-Platform/Embedded Thread Design

Jason Anderson

Debugging multi-threaded code is hard enough. You may as well do as much debugging as possible in a comfortable environment.


When I'm developing code for an embedded system, I like to write and test as much code as possible on my personal machine. I find it especially useful to test the code on my machine before I commit it to flash RAM, or tie up the embedded system for testing. The advantages of this kind of "local" embedded system development are fairly obvious, but in practice it can be difficult to accomplish. Not only may the local machine lack the embedded system's specialized hardware — indeed, it may have a completely different architecture — but it may be running a different operating system as well. In a sense then, developing embedded code locally is a special case of cross-platform development and testing. If you follow an effective strategy for cross-platform development, you'll go a long way toward solving the problems of local embedded development as well.

Half the challenge of cross-platform design is selecting the appropriate set of classes to be defined; the rest of the design consists of correctly selecting functionality to be abstracted. Locating the classes that make lots of system-specific calls serves as an excellent starting point for abstraction. For example, an embedded TCP/IP application is guaranteed to make calls to socket functions, which may vary from OS to OS. A board populated with a library for reading external sensors obviously will require wrapping as well.

This article presents a cross-platform thread class that works transparently across UNIX and Win32 operating systems. The design strategy I employed is pretty basic: develop a base thread class that defines a common interface for all platforms, and inherit from the base class to specialize the threads to the platforms in question. What I would like to point out, however, is that this strategy works well for developing software which, while targeted for an embedded platform, can be tested on your local machine.

Defining the AThread API

When developing cross-platform libraries, a good place to start is to identify the functionality that will be needed on multiple platforms. In this particular case, I want to provide a single class for creating and controlling threads (I call it AThread), regardless of the current platform. A simple design approach, which I adopt here, is to treat each thread as a C++ object. AThread (see Listing 1) creates and terminates a thread via respective construction and destruction of an AThread object. AThread objects can be manipulated by other threads. For example, AThread provides methods that allow any other thread with access to the object to kill the thread — even though this action is intended specifically for use by the parent thread. (The parent thread is the thread that started the thread in question. This is not to be confused with the parent class, from which the thread's class is derived.)

After an AThread object is constructed, AThread::Run jump-starts the thread's executable code, which in effect reroutes the application's flow of execution. An AThread object also responds to various state information requests, allowing the host to probe the current state of a thread (running, dead, or pending termination). To implement a thread, an application must derive from AThread, and supply an overridden Run member function.

Since a thread is represented by an object, the parent thread contains a pointer to the thread object and thus can interact with it like any other object. The parent thread will typically be a derived class object of the same type as its child thread. This allows threads to have an excellent and intuitive method of communication. Any derived thread class can easily provide public behaviors (via new member functions) to support any method of conversation desired. (Bear in mind, however, that synchronization issues will still exist.)

Platform-Specific Implementations

After defining the thread control API through the abstract base class, the only step left is to derive classes for all target platforms. For local testing of embedded code, I need to implement threads for only two platforms. Each of the target systems uses a class derived from AThread (AWin32Thread and AUNIXThread), containing the platform-specific code (see Listings 2, 3, 4, 5) .

As much as possible, I've tried to maintain identical behavior between AWin32Thread and AUNIXThread. For example, instantiating either type of object correctly spawns a new thread, and the Kill member function does what its name suggests. However, providing identical behavior is not always easy.

Consider the methods used to kill a thread under Win32 and UNIX. While the Win32 API provides KillThread() as an easy way to kill any thread in the system, UNIX allows only for exiting the current thread. (I am ignoring here KillThread's tendency to leave things in a messy state.) The UNIX implementation, therefore, places some restrictions on Kill's behavior. Calling the UNIX Kill function sets a state variable, to indicate to the thread that it has been asked to shut down. The AUNIXThread object (and thus, any class derived thereof) must therefore occasionally check the thread's state variable. Applications can check this variable by calling AThread::CheckStatus, which automatically terminates the thread when the state variable has the value TERMINATE.

Designing for local testing of some embedded applications calls for a bit more creativity. For example, the target system may contain proprietary data acquisition hardware that is not available in your local testing environment. This proprietary hardware is typically controlled via special system calls. You can still provide a platform-independent interface that includes the special system call APIs. If a platform lacks the proprietary hardware, simply fabricate the data and responses that would have been generated if the hardware were present. This technique is particularly useful for developing any system that uses custom hardware interfaces. Using a virtual interface allows the main application to be developed using simulated hardware components, which may be weeks or months away from availability.

Using AThread

To create a thread of execution using AThread, simply derive a new class from your target system's thread class and supply a Run member. This will define a custom thread that can be instantiated and controlled with the AThread interface. However, remember that threads automatically terminate whenever their associated object is destroyed. So in most situations you should use explicitly allocated thread objects; stay away from the stack!

More sophisticated programs often require additional interfaces in the derived class, to serve as communication doorways between the parent and child threads. For example, programs often need a way to lock and unlock pieces of data, so as to synchronize data access. I've provided a simple procedure in AThread for locking and unlocking the entire object; only a single thread may claim a lock at any time. AThread::Lock and AThread::Unlock provide only one mutex semaphore per object, but since it is defined in the AThread interface it is guaranteed to work on any compliant platform.

Show Time!

With my separate implementations in place, I can now easily test applications using AThread on either of the supported platforms at any time. Listing 6 shows the sample ANetThread application thread. I like to perform most of my testing locally under Windows NT, so most of the time I derive the ANetThread from AWin32Thread. When it's time to test "the real thing," all I need to do is change one line of code and recompile! By simply deriving ANetThread from AUNIXThread instead of AWin32Thread, I can easily bounce back and forth between systems.

Better yet, by deriving ANetThread from a compile-dependent class, I can specify the target platform with a single #define statement (see the sample program in Listings 7 and 8) . This sample program uses ANetThread to establish a TCP connection to send text messages between two workstations. Changing TARGET_PLATFORM in target.h from WIN32 to UNIX will automatically select the proper thread class and include files to build the correct version of your application. This method easily extends across larger projects, perhaps controlling the target of several different cross-platform libraries.

Because ANetThread conforms to the AThread interface, it will behave correctly when derived from any compliant AThread implementation. Similarly, any application that instantiates ANetThread (or any other AThread-derived class) can control it through the base class' interface. This has the benefit of hiding from the host any specifics about how threads are managed on the current operating system. It also makes it possible to write a new thread class that is completely ignorant of the final target system.

Summary

Using abstract base classes (a.k.a. interface classes) to provide a common interface is a well-established technique in cross-platform application design. This technique can also be useful in embedded systems, especially when testing embedded applications on non-target systems.

Using interface classes also conveniently protects you from problems beyond your control, namely unexpected changes in project requirements. If six months into development you suddenly find that your target platform is now VxWorks instead of SunOS, it will not be a major headache to get these sensitive code segments up and running. o

J. Scott Anderson works as a software engineer at Vivid Technologies, Inc. He has been programming since 1980, specializing in computer graphics and network applications. He can be reached at jayscott@tiac.net.