Chapter 5: Developing Phase: Infrastructure Services

This chapter discusses the potential coding differences between UNIX and Microsoft® Windows® operating systems with respect to infrastructure services.

Infrastructure services are discussed in the following sections:

  • Security

  • Handles

  • Error and Exception Handling

  • Handles

  • Signals vs. Events

  • Interprocess Communication (IPC)

  • Networking

This chapter describes the implementation of the preceding infrastructure services in the Windows environment and provides a detailed comparison with the corresponding implementation in UNIX.

Using the information provided in this chapter, you can identify any incompatibilities in your applications in these areas and also learn about the suggested replacement mechanisms for the Windows environment.

*

On This Page

Security Security
Handles Handles
Error and Exception Handling Error and Exception Handling
Signals vs. Events Signals vs. Events
Interprocess Communication (IPC) Interprocess Communication (IPC)
Networking Networking
Miscellaneous Features Miscellaneous Features

Security

The UNIX and Windows security models are quite different. The Windows application programming interface (API) uses the underlying Windows security model. This results in key differences between the way Windows security works and the way UNIX security works. Some of these differences are covered in the “Architectural Differences” section in Chapter 1, “Introduction to Win32/Win64” of this volume. This chapter covers the differences in their respective security models. This section describes various security features available in Windows, such as user-level security and process-level security, and compares them with UNIX. With this information, you can modify the security-related code in your applications to operate in Windows.

User-Level Security

This section discusses the differences in implementing user-level security in UNIX and Windows.

Retrieving the User Name of the Current User

The following examples illustrate the migration of code from UNIX to Windows to retrieve the user name of the current user.

UNIX example: Retrieving the user name of the current user

This example uses the getlogin function to retrieve the user name of the user currently logged onto the system.

#include <stdio.h>
#include <unistd.h>
main()
{
// Get and display the user name.
printf("User name: %s\n", getlogin());
}

(Source File: U_GetUser-UAMV3C5.01.c)

Windows example: Retrieving the user name of the current user

This example uses the GetUserName function to retrieve the user name of the current thread. This is the name of the user currently logged on to the system.

The GetUserNameEx function can be used to retrieve the user name in a specified format.

#include <windows.h>
#include <stdio.h>
#include <lmcons.h>
void main()
{
LPTSTR lpszSystemInfo; // pointer to system information string
DWORD cchBuff = 256; // size of user name
TCHAR tchBuffer[UNLEN + 1]; // buffer for expanded string
lpszSystemInfo = tchBuffer;
// Get and display the user name.
GetUserName(lpszSystemInfo, &cchBuff);
printf("User name: %s\n", lpszSystemInfo);
}

(Source File: W_GetUser-UAMV3C5.01.c)

Process-Level Security

In UNIX, access rights are determined by the permissions on files. Each user has a UID, which (unlike Windows) does not have to be unique. A user is logged on to the system when a shell process is running that has the UID of the user. Groups are sets of users. A UNIX group has a Group ID (GID). Every process has a UID and a GID associated with it.

When a user logs on to the system with a user name and a password, UNIX starts a shell with the UID and GID of that user. From then on, all access to files and other resources is controlled by the permissions assigned to the UID and GID or the process. The UIDs and GIDs are configured in two files: /etc/passwd and /etc/group.

Security permissions are applied to files based on users or groups. The permissions that can be granted are read, write, and execute. These permissions are grouped in three sets: the owner of the file, the group of the owner, and everyone else. A full (long) listing for a file shows the file permissions as a group of nine characters that indicate the permissions for owner, group, and everyone. The characters r, w, x, and - are used to indicate read, write, execute, and no permission, respectively. For example, if the owner of a file has all permissions but the group and everyone have only read permission, the string is as follows:

rwxr--r--

Note Some UNIX implementations have extended the basic security model to include access control lists (ACLs) similar to those used in Windows. However, ACLs are not implemented consistently across all versions of UNIX.

A process can take the identity of another user in order to gain the access permissions of that user or to use resources that may be accessible to the other user. This can be done using the setuid function in UNIX. The following example changes the real, effective, and saved-set-UIDs.

UNIX example: Set the UID of the process to a specific user

#include <unistd.h>
#include <stdio.h>
int main(void) 
{
printf ( "prior to setuid(), uid = %d, effective uid = %d\n"
(int) getuid(), (int) geteuid() );
if ( setuid(25) != 0 )
perror( "setuid() error" );
else
printf ( "after setuid(),     uid = %d, effective uid = %d\n", 
(int) getuid(), (int) geteuid() );
    return 0;
}

(Source File: U_SetUid-UAMV3C5.01.c)

Windows uses a unified security model that protects all objects from unauthorized access. The system maintains security information for the following:

  • Users. The people who log on to the system, either interactively by entering a set of credentials (user name and password) or remotely through the network. The security context of every user is represented by a logon session. Each process that the user starts is associated with the logon session of the user.

  • Objects. The secured resources that a user can access. For example, files, synchronization objects, and named pipes represent kernel objects.

Access Tokens

An access token is a data structure associated with every process that is started by a particular user and is associated with the logon session of that user. The access token identifies who the user is and which security groups he or she belongs to. Although users and groups have human-readable names to ease administration, for performance reasons they are uniquely identified, internally, by security identifiers (SIDs).

Security Descriptors

A security descriptor describes the security attributes of each object. The information in the security descriptor includes the owner of the object, the system access control list (SACL), and a discretionary access control list (DACL). The DACL contains a list of access control entries (ACEs) that defines the access rights for particular users or groups of users. The owner of the object controls the DACL and uses it to determine who should and should not be allowed access to the object, and what rights should be granted to them.

The security descriptor also includes a system access control list (SACL), which is controlled by system administrators. Administrators use SACLs to specify auditing requirements for object access. For example, an administrator can establish a SACL that specifies the generation of an audit log entry whenever a user attempts to delete a particular file.

The sequence of events from the time a user logs on, to the time the user attempts to access a secure object, is as follows:

  1. The user logs on with a set of credentials. The system validates these credentials by comparing them against the information maintained in a security database (or Microsoft Active Directory® directory service).

  2. If the details of the user are authenticated, the system creates a logon session that represents the security context for the user. Every process created on behalf of the user (starting with the Windows shell process) contains an access token that describes the security context of the user.

  3. Every process subsequently started by the user is passed a copy of the access token. If one process results in additional processes, all child processes obtain a copy of the access token and are associated with the single logon session of the user.

  4. When a process (acting on behalf of the user) attempts to open a secure object such as a file, the process must initially obtain a handle to the object. For example, when attempting to open a file, the process calls the CreateFile function. The process specifies a set of access rights on the call to CreateFile.

  5. The security system accesses the security descriptor of the object and uses the list of ACEs contained in the DACL to find a group or user SID that matches the one contained in the access token of the process. When this task is complete, the user is either denied access to the object (if a deny ACE is located) or the user is granted a specific set of access rights to the object. The granted rights may be the same as the rights initially requested or they may be a subset of the rights initially requested. For example, the CreateFile call can request read and write access to a file, but the DACL may allow only read access.

Impersonation

When a thread within a process attempts to access a secured object, the security context that represents the user who is making the access attempt is normally obtained from the process-level access token. You can, however, associate a temporary access token with a specific thread.

For example, within a server process, you can impersonate the security context of a client. The act of impersonation associates a temporary access token with the current thread. The temporary impersonation access token represents the security context of the client. As a result, the server thread uses the security context of the user when it attempts to access any secured object. When the temporary access token is removed from the thread, impersonation ceases and subsequent resource access reverts to using the process-level access token.

The ImpersonateLoggedOnUser function can be used for this. The user is represented by a token handle.

BOOL ImpersonateLoggedOnUser(
HANDLE hToken
);

Here, hToken is a handle to a primary or impersonation access token that represents a logged-on user. This can be a token handle returned by a call to the LogonUser, CreateRestrictedToken, DuplicateToken, DuplicateTokenEx, OpenProcessToken, or OpenThreadToken functions.

The impersonation lasts until the thread exits or until it calls RevertToSelf. The calling thread does not need to have any particular privileges to call ImpersonateLoggedOnUser.

Handles

This section discusses the differences in implementing system handles in UNIX versus Windows. This section covers the following topics:

  • Socket handles

  • File handles

  • Output buffer or event queue handling

By understanding the implementation of handles in Windows and UNIX, you will be able to identify the unsupported handle-specific features in the UNIX application and the replacement mechanisms in the Windows environment specific to signal and error handling.

Socket Handles

In UNIX, socket handles are small, non-negative integers. Socket handles can be passed to most of the low-level Portable Operating System Interface (POSIX) input/output (I/O) functions. For example:

read(s, buffer, buffer_len);

In the earlier example, s could either be a socket or a file handle. It is common to use read instead of recv to read data from a socket, for example. Similarly, the write function call is equivalent to send and sendto.

Windows defines a new unsigned data type SOCKET that may take any value in the range 0 to INVALID_SOCKET–1, where INVALID_SOCKET is a predefined value for a nonexistent socket. Because the SOCKET type is unsigned, compiling existing source code from a UNIX environment may lead to compiler warnings about signed/unsigned data type mismatches.

A socket handle can optionally be a file handle in Windows Sockets 2. It is possible to use socket handles with the ReadFile, WriteFile, ReadFileEx, WriteFileEx, DuplicateHandle, and other functions. However, for an application to run over the widest possible number of service providers, it should not assume that socket handles are file handles.

File Handles

In UNIX, a file handle is an opaque number that is used to uniquely identify a file or other file system objects.

The fact that the file handle is opaque means that no information can be obtained from the file by inspecting the contents of the file handle. The only operations that can be done with the file handle are to copy and compare it for equality with another file handle.

The traditional contents of a file handle are:

  • An identifier for the file system, such as the device number from which the file system is mounted.

  • An identifier for the inode within the file, such as the inode number.

  • A field to indicate when an inode has been reused; this is typically called a generation number for the inode.

In Windows, the file handle is used to identify a file. When a file is opened by a process using the CreateFile function, a file handle is associated with it until either the process terminates or the handle is closed using the CloseHandle function.

Each file handle is generally unique to each process that opens a file. The only exceptions to this are when a file handle held by a process is duplicated or when a child process inherits the file handles form the parent process. These file handles are unique, but they refer to a single, shared file object.

Note Although the file handles are typically private to a process, the file data that the file handles point to is not. Therefore, processes and threads that share the same file must synchronize their access. For most operations on a file, a process identifies the file through its private pool of handles.

Output Buffer or Event Queue Handling

The operating system may respond to events immediately or put them in an EventQueue for later processing. The select function can be used in UNIX for this. This function indicates which of the specified file descriptors are ready for reading or writing or which have an error condition pending. If none of the specified file descriptors is ready for reading/writing or has an error condition pending, the select function keeps blocking itself for a predefined timeout interval until one of the file descriptors is ready. The select function supports regular files, terminal and pseudo-terminal devices, stream-based files, FIFOs, and pipes.

In Windows, this is possible with the WaitForMultipleObjects or WaitForSingleObject functions as well. The WaitForMultipleObjects function determines whether the given input objects meet the wait criteria. It returns a value when either of the specified objects is in the signaled state or when the time-out interval elapses. WaitForSingleObject also behaves in the same way as WaitForMultipleObjects except that it works with the single input object. If the criteria are not met, the calling thread enters the wait state. It does not use any processor cycles while waiting for the criteria to be met.

These functions can specify handles of any of the following object types as the input parameter:

  • Change notification

  • Console input

  • Event

  • Job

  • Memory resource notification

  • Mutex

  • Process

  • Semaphore

  • Thread

  • Waitable timer

Use caution when calling the wait functions and code that directly or indirectly creates windows. If a thread creates any window, it must process messages. Message broadcasts are sent to all windows in the system. A thread that uses a wait function with no time-out interval may cause the system to enter a deadlock. Therefore, if you have a thread that creates windows, use the MsgWaitForMultipleObjects or MsgWaitForMultipleObjectsEx function instead of WaitForMultipleObjects. There is no corresponding function to WaitForSingleObject in this context.

The WaitForMultipleObjects function takes a HANDLE*, but HANDLEs to files and sockets are not allowed to be in that array. Instead, asynchronous I/Os can take synchronization objects as input parameters, which in turn can be made to wait using the WaitForMultipleObjects function. The WaitForMultipleObjectEx can wake the thread automatically with a special result when any asynchronous I/O scheduled by the calling thread is completed.

An alternative solution would be that all communication is performed through file descriptors. This requires modifying the operating system for something as simple as adding a new EventQueue. WinSocket solves that by putting all socket events in the window EventQueue. All petitions made to the operating system are responded to through the event queue. Each event has all the necessary information to know what needs to be done with the result.

Error and Exception Handling

In UNIX, when an error occurs in a function, a negative value is often returned and the integer errnois usually set to a value that gives information about the error. The file errno.h defines the variable errno and the constants for each value that errno can assume. The application can retrieve and set the last error with the errno value. The function strerror gets the error message, and the perror *function produces the error message on the standard error stream based on the value of errno.

In Windows, when an error occurs, most functions return an error code, usually zero, NULL, or a negative value. Functions can also set an internal error code called the last-error code. The SetLastError function sets the last-error code for the calling thread. Information on the last error that occurred can be gotten from the function GetLastError. To retrieve the description text for the error in your application, use the FormatMessage function with the FORMAT_MESSAGE_FROM_SYSTEM flag. The last-error code is kept in the thread local storage (TLS) so that multiple threads do not overwrite each other's values. The signature of the functions is defined as follows.

Note: Some of the lines in the following code have been displayed on multiple lines for better readability.

void SetLastError(DWORD dwErrCode);
DWORD GetLastError(void);
DWORD FormatMessage(DWORD dwFlags, LPCVOID lpSource,
DWORD dwMessageId, DWORD dwLanguageId, LPTSTR 
lpBuffer, DWORD nSize, va_list* Arguments);

Error codes are defined in the WinError.h* header file. Error codes are 32-bit values (bit 31 is the most significant bit), as in UNIX. However, the error codes and names are different in UNIX and Windows. UNIX error names begin with an E *and Windows error names begin with ERROR_.

Note More information on error codes and error handling in Windows is available at “Debugging and Error Handling” topic on MSDN®.

Signals vs. Events

This section maps the signals in UNIX to their equivalent objects in Windows. In addition, it provides different alternatives for converting UNIX code to Windows code and suggests the pros and cons of each alternative. The following topics are discussed in this section:

  • Using native signals in Windows.

  • Replacing UNIX signals with Windows messages.

  • Replacing UNIX signals with Windows event objects.

  • Porting the Sigaction call.

This section provides you with information regarding signal-specific implementation in your UNIX application and provides the best replacements, such as native signals, messages, and event objects, for the Windows environment.

The UNIX operating system supports a wide range of signals. UNIX signals are software interrupts that catch or indicate different types of events. Windows, on the other hand, supports only a small set of signals that is restricted to exception events. Consequently, converting UNIX code to Windows requires the use of new techniques to replace the use of some UNIX signals.

The Windows signal implementation is limited to the signals listed in Table 5.1.

Table 5.1. Windows Signals

Signal

Meaning

SIGABRT

Abnormal termination

SIGFPE

Floating-point error

SIGILL

Illegal instruction

SIGINT

CTRL+C signal

SIGSEGV

Illegal storage access

SIGTERM

Termination request

Note When a CTRL+C interrupt occurs, Windows generates a new thread to handle the interrupt. This can cause a single-thread application, such as one ported from UNIX, to become multithreaded, which may result in an unexpected behavior.

The signal function allows a process to choose one of the several ways to handle an interrupt signal from the operating system. The sig argument is the interrupt to which the signal responds. The argument must be one of the manifest constants defined in SIGNAL.H.

By default, signal terminates the calling program with exit code 3, regardless of the value of sig.

When an application uses other signals not supported in Windows, you have the option of using Messages or Events. This section focuses on the Windows mechanisms that you can use to replace some UNIX signals.

Table 5.2 lists the recommended mechanisms that you can use to replace common UNIX signals. Following are the three main mechanisms:

  • Native signals

  • Messages

  • Event objects

    Table 5.2. UNIX Signals and Replacement Mechanisms

    Signal name

    Description

    Suggested Replacement on Windows

    SIGABRT

    Abnormal termination

    SIGABRT

    SIGALRM

    Time-out alarm

    SetTimer – WM_TIMER - CreateWaitableTimer

    SIGCHLD

    Change in status of child

    WaitForSingleObject

    SIGCONT

    Continue stopped process

    WaitForSingleObject

    SIGFPE

    Floating point exception

    SIGFPE

    SIGHUP

    Hang-up

    NA

    SIGILL

    Illegal hardware instruction

    SIGILL

    SIGINT

    Terminal interrupt character

    SIGINT

    SIGKILL

    Termination

    WM_QUIT

    SIGPIPE

    Write to pipe with no readers

    WaitForSingleObject

    SIGQUIT

    Terminal Quit character

    WM_CHAR

    SIGSEGV

    Invalid memory reference

    SIGSEGV

    SIGSTOP

    Stop process

    WaitForSingleObject

    SIGTERM

    Termination

    SIGTERM

    SIGTSTP

    Terminal Stop character

    WM_CHAR

    SIGTTIN

    Background read from control tty

    NA

    SIGTTOU

    Background write to control tty

    NA

    SIGUSR1

    User-defined signal

    SendMessage – WM_APP

    SIGUSR2

    User-defined signal

    SendMessage – WM_APP

Note Only POSIX signals are considered in this table. Seventh Edition, System V, and BSD signals have been excluded.

Another mechanism that can be useful when converting some UNIX signals to Windows is event kernel objects.

Using Native Signals in Windows

In the following example, the simple case of catching SIGINT to detect CTRL-C is demonstrated. As you can see from the two examples, support for handling native signals in UNIX and Windows is very similar.

UNIX example: Managing signals

#include <unistd.h>
#include <stdio.h>
#include <signal.h>
/* The intrpt function reacts to the signal 
/* passed in the parameter signum.
This function is called when a signal occurs.
A message is output, then the signal handling for 
SIGINT is reset
(by default generated by pressing CTRL-C) back to 
the default behavior.
*/
void intrpt(int signum)
{
printf("I got signal %d\n", signum);
(void) signal(SIGINT, SIG_DFL);
}
/* main intercepts the SIGINT signal generated 
when Ctrl-C is input.
Otherwise, sits in an infinite loop, printing a 
message once a second.
*/
int main()
{
(void) signal(SIGINT, intrpt);
while(1) {
printf("Hello World!\n");
sleep(1);
}
}

(Source File: U_ManageSignl-UAMV3C5.01.c)

Windows example: Managing signals

Note: Some of the lines in the following code have been displayed on multiple lines for better readability.

#include <windows.h>
#include <signal.h>
#include <stdio.h>
void intrpt(int signum)
{
printf("I got signal %d\n", signum);
(void) signal(SIGINT, SIG_DFL);
}
/* main intercepts the SIGINT signal generated 
/* when Ctrl-C is input.
Otherwise, sits in an infinite loop, printing a 
message once a second.*/
void main()
{
(void) signal(SIGINT, intrpt);
while(1) 
{
printf("Hello World!\n");
Sleep(1000);
}
}

(Source File: W_ManageSignl-UAMV3C5.01.c)

Note By default, the signal terminates the calling program with exit code 3, regardless of the value of sig. Additional information is available at https://msdn.microsoft.com/library/default.asp?url=/library/en-us/vclib/html/_crt_signal.asp.

With the exception of requiring an additional header file and the different signature of the sleep function, the two examples are identical.

Replacing UNIX Signals with Windows Messages

UNIX uses signals to send alerts to processes when specific actions occur. A UNIX application uses the kill function to activate signals internally. As discussed earlier, Windows provides only limited support for signals. As a result, you must rewrite your code to use another form of event notification in Windows.

The following example illustrates how you can convert UNIX code to Windows messages or event objects. It shows a simple main function that forks a child process, which issues the SIGALRM signal. The parent process catches the signal and outputs a message when it is received.

UNIX example: Using the SIGALRM signal

Note: Some of the lines in the following code have been displayed on multiple lines for better readability.

#include <unistd.h>
#include <stdio.h>
#include <signal.h>
static int alarm_fired = 0;
/* The alrm_bell function simulates an alarm clock. */
void alrm_bell(int sig)
{
alarm_fired = 1;
}
int main()
{
int pid;
/* Child process waits for 5 sec's before sending
SIGALRM to its parent. */
printf("alarm application starting\n");
if((pid = fork()) == 0) 
{
sleep(5);
kill(getppid(), SIGALRM);
exit(0);
}
/* Parent process arranges to catch SIGALRM 
with a call to signal
and then waits for the child process to 
send SIGALRM. */
printf("waiting for alarm\n");
(void) signal(SIGALRM, alrm_bell);
pause();
if (alarm_fired)
printf("Ring...Ring!\n");
printf("alarm application done\n");
exit(0);
}

(Source File: U_SigAlrm-UAMV3C5.01.c)

In the example that follows, a form of Microsoft Windows messages is used to signal the parent process. The SetTimer function is used to signal to the parent process that an alarm has been activated. Although code can be created to do the timing, using the SetTimer function greatly simplifies this example.

Another advantage of using SetTimer is that the callback function is invoked in the same thread that calls SetTimer; therefore no synchronization is necessary.

If the requirements are simple, consider using a thread to act as a timer thread that calls Sleep to create the desired delay. At the end of the delay, a call is made to a timer callback function. The problem with this approach is that the callback function is not called from your primary thread. If the callback function requires resources that are thread-specific, use one of the appropriate synchronization mechanisms discussed in the “Synchronization of Threads” section in Chapter 3: “Process and Thread Management” of this volume.

Additional code is added to the example so that an application using this code can catch any standard Windows message as well as application and user-defined messages. You can use these messages to engineer solutions to other signals that are not directly supported by the native signal implementation in Windows.

Windows example: Replacing SIGALRM using messages

Note: Some of the lines in the following code have been displayed on multiple lines for better readability.

#include <windows.h>
#include <stdio.h>
#include <conio.h>
#include <stdlib.h>
static int alarm_fired = 0;
/* The alrm_bell function simulates an alarm clock. */
VOID CALLBACK alrm_bell(HWND hwnd, UINT uMsg, UINT 
idEvent, DWORD dwTime )
{
alarm_fired = 1;
printf("Ring...Ring!\n");
}
void main()
{
printf("alarm application starting\n");
/* Set up a 5 second timer which calls alrm_bell */
SetTimer(0, 0, 5000, (TIMERPROC)alrm_bell);
printf("waiting for alarm\n");
MSG msg = { 0, 0, 0, 0 };
/* Get the message, & dispatch. This causes 
alrm_bell to be invoked. */
while(!alarm_fired)
if (GetMessage(&msg, 0, 0, 0) ) 
{
if (msg.message == WM_TIMER)
printf("WM_TIMER\n");
DispatchMessage(&msg);
}
printf("alarm application done\n");
exit(0);
}

(Source File: W_SigAlrm-UAMV3C5.01.c)

Notice in this example that the WM_TIMER message is issued by the SetTimer function and captured by the GetMessage function. If you remove the call to DispatchMessage, the alrm_bell function is never called, but the WM_TIMER message is received. With this simple application, you can capture a variety of Windows messages. Moreover, if you want to trigger the callback function before the specified time, you can use the PostMessage(WM_TIMER) call. This is analogous to using the kill function to send a signal in UNIX.

Replacing UNIX Signals with Windows Event Objects

Some events that UNIX handles through signals are represented in Windows as event objects. Functions are available to integrate these event objects. An example of these functions is the WaitForSingleObject function.

In the example code that follows, a timer object is used to signal when a timed interval has elapsed. Again, this example provides the same functionality as the preceding UNIX SIGALRM example.

Note While this illustration encompasses the process in a single thread, this is not a requirement. The timer object can be tested and waited for in other threads if necessary.

Windows example: Replacing SIGALRM using event objects

#define _WIN32_WINNT 0X0500
#include <windows.h>
#include <stdio.h>
#include <conio.h>
#include <stdlib.h>
void main()
{
HANDLE hTimer = NULL;
LARGE_INTEGER liDueTime;
liDueTime.QuadPart = -50000000;
printf("alarm application starting\n");
// Set up a 5 second timer object
hTimer = CreateWaitableTimer(NULL, TRUE, "WaitableTimer");
SetWaitableTimer(hTimer, &liDueTime, 0, NULL, NULL, 0);
// Now wait for the alarm
printf("waiting for alarm\n");
// Wait for the timer object
WaitForSingleObject(hTimer, INFINITE);
printf("Ring...Ring!\n");
printf("alarm application done\n");
exit(0);
}

(Source File: W_SigAlrm-UAMV3C5.02.c)

Porting the Sigaction Call

Windows does not support the sigaction function. The UNIX example that follows shows how the sigaction function is typically used in a UNIX application. In this example, the handler for the SIGALRM signal is set. Conversion of this code to use Windows messages was shown earlier. For the corresponding Windows example, refer to the previous section “Replacing UNIX Signals with Windows Messages.”

Note To terminate the following application from the keyboard, press CTRL+\.

UNIX example: Using sigaction

#include <unistd.h>
#include <stdio.h>
#include <signal.h>
void intrpt(int signum)
{
printf("I got signal %d\n", signum);
}
int main()
{
struct sigaction act;
act.sa_handler = intrpt;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGINT, &act, 0);
while(1) 
{
printf("Hello World!\n");
sleep(1);
}
}

(Source File: U_SigActn-UAMV3C2.01.c)

Interprocess Communication (IPC)

Like UNIX, Windows has various forms of interprocess communication (IPC). Following are the IPC forms that are most familiar to UNIX developers:

  • Process pipes

  • Named pipes

  • Message queues

  • Sockets

  • Memory-mapped files

  • Shared memory

  • Remote procedure call (RPC)

Windows supports implementations of all of these forms except for message queues. Windows provides two additional IPC mechanisms: remote procedure call (RPC) and mailslots. RPC is designed for use by client/server applications and is most appropriate for C and C++ programs. Mailslots are memory-based files that a program can access using standard file functions. The maximum size of mailslots is fairly small.

In addition to these mechanisms, there are two forms of IPC that are not part of the Windows API. These are Message Queuing (also known as MSMQ) and COM+.

This section looks at how UNIX code that uses different forms of IPC can be converted to Windows IPC techniques. It also introduces new methods of IPC that are not available in UNIX  but may provide a better solution for interprocess communication of your application. You can use this information to analyze the IPC implementations in your UNIX application and to identify the best porting approach for corresponding IPC implementation in the Windows environment.

Pipes (Unnamed or Named, Half or Full Duplex)

Pipes have similar functionality on both Windows and UNIX systems. Their primary use is to communicate between related processes. UNIX pipes can be named or unnamed. They also have separate read and write file descriptors, which are created through a single function call. With unnamed pipes, a parent process that must communicate with a child process creates a pipe that the child process will inherit and use. Two unrelated processes can use named pipes to communicate.

Windows pipes can also be named or unnamed. A parent process and a child process typically use unnamed pipes to communicate. Unnamed pipes are half duplex. The processes must create two unnamed pipes for bidirectional communication. Two unrelated processes can use named pipes even across the network on different computers. Typically, a server process creates the pipe, and clients connect to the bidirectional pipe to communicate with the server process. Named pipes are full duplex.

Process Pipes

Process pipes are supported in Windows by using the standard C run-time library. As discussed in the following sections, they are largely equivalent to process pipes in UNIX. Three UNIX examples are considered in this section, and a slightly modified Windows version of each is also provided.

High-Level popen Call

Note that this text assumes the presence of the Uname.exe executable program on the Windows-based system. If your system does not contain this executable, these samples will not work, and you must modify them to use an equivalent tool.

In the first example of process pipes, a process called uname is executed and passes the output of this process to standard output. As you review these examples, notice that the differences between the UNIX and Windows implementations are the header files that are required and the function names for popen and pclose. The names of these functions in Windows are preceded by an underscore ( _ ). The function syntax is the same and the behavior is largely compatible.

Low-Level pipe Call

This section demonstrates the process of converting code that uses the pipe function call to communicate between two parts of an application. The following example demonstrates how you can write to one end of the pipe (fd[1]) and read from the other (fd[0]). This is an example of a half duplex pipe. The differences between the UNIX and the Windows implementations are the required header files and the underscore that precedes the pipe function.

However, in this case, an additional modification must be made for the solution to work on Windows. If you look at the online documentation for pipe, you will notice that it requires two additional arguments. Providing these arguments specifies the amount of memory that must be used as a buffer for the pipe and the mode of the pipe (O_TEXT or O_BINARY).

UNIX example: Low-level pipe call

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
int data_out, data_in, file_pipes[2];
const char data[] = "ABCDE";
char buffer[BUFSIZ + 1];
memset(buffer, '\0', sizeof(buffer));
if (pipe(file_pipes) == 0) 
{
data_out = write(file_pipes[1], data, strlen(data));
printf("Wrote %d bytes\n", data_out);
data_in = read(file_pipes[0], buffer, BUFSIZ);
printf("Read %d bytes: %s\n", data_in, buffer);
exit(EXIT_SUCCESS);
}
exit(EXIT_FAILURE);
}

(Source File: U_LowLvlPip-UAMV3C5.01.c)

Windows example: Low-level pipe call

#include <windows.h>
#include <stdlib.h>
#include <stdio.h>
#include <io.h>
#include <fcntl.h>
#include <process.h>
void main()
{
size_t data_out, data_in;
int file_pipes[2];
const char data[] = "ABCDE";
char buffer[BUFSIZ + 1];
memset(buffer, '\0', sizeof(buffer));
if (_pipe(file_pipes, 32, O_BINARY) == 0) 
{
data_out = write(file_pipes[1], data, strlen(data));
printf("Wrote %d bytes\n", data_out);
data_in = read(file_pipes[0], buffer, BUFSIZ);
printf("Read %d bytes: %s\n", data_in, buffer);
exit(EXIT_SUCCESS);
}
exit(EXIT_FAILURE);
}

(Source File: W_LowLvlPip-UAMV3C5.01.c)

Named Pipes (FIFOs)

In this section, a few examples of named pipes are shown. These are sometimes referred to as first-in-first-out (FIFO).

Interprocess Communication with Named Pipes

To show how you can convert code using FIFO from UNIX to Windows, a simple example that creates a named pipe is shown. This example uses minimal security restrictions for simplicity. The example uses the mkfifo function in UNIX and the CreateNamedPipe function in Windows. There are considerable differences between these two functions. Both functions have the same purpose, but the CreateNamedPipe function offers a greater degree of control over the configuration of the pipe. The major difference between UNIX and Windows pipes exists in security attributes, buffering, and opening modes. The mkfifo() system call creates a new FIFO file with name path. The access permissions are specified by mode and restricted by the umask routine of the calling process. In the case of Windows pipes, the access control list (ACL) from the security attributes parameter defines the discretionary access control for the named pipe. If SecurityAttributes is NULL, the default security descriptor and the handle cannot be inherited. The ACLs in the default security descriptor for a named pipe grant full control to the LocalSystem account, administrators, the creator owner, and read access to members of the Everyone group and the anonymous account. The details are beyond the scope of this guide, but you can find the links for CreateNamedPipe on the MSDN Web site.

Note Additional information on named pipes on Windows is available at https://msdn.microsoft.com/library/default.asp?url=/library/en-us/ipc/base/named_pipe_type_read_and_wait_modes.asp.

Back-Pressure in Pipes

There is an important difference between pipes in UNIX and Windows involving the concept of back-pressure. UNIX uses the buffer concept for dealing with programs that use pipes whereas Windows uses the file system object. This difference becomes a major point to consider when migrating to Windows if the program in question has a tendency to acquire back-pressure. This issue arises in sequences of programs connected by pipes because there is a finite capacity for the pipe in UNIX versus a Windows file system object that will almost never be exhausted, based on page-file size.

Note The point of the guidance is that a set of UNIX applications, which rely on pipe back-pressure to throttle a process, should be reexamined when moved to use native Windows pipes. Instead of relying on back-pressure, a more explicit mechanism should be selected by redesigning the application to remove its dependency on back-pressure.

Shared Memory

Shared memory is an efficient means of passing data between programs. One program will create a memory portion, which other processes (if permitted) can access.

UNIX supports System V shared memory, which allows multiple processes to attach a segment of physical memory to their virtual address spaces. When write access is allowed for more than one process, an outside protocol or mechanism such as a semaphore can be used to prevent inconsistencies and collisions. The other efficient way to implement shared memory in UNIX applications is to rely on the mmap function and on the native virtual memory facility of the system. The mmap function establishes a mapping of a named file system object (or part of one) into a process address space.

Windows implementation of shared memory can be done using the concept of file mapping or by making use of memory in a heap using the GlobalAlloc function. A common section of memory can be mapped into the address space of multiple processes. If no file is specified in the creation function, the shared memory is allocated from a section of the page file. As in the UNIX implementation, which uses an identifier, Windows uses a handle identifier to identify the memory that is mapped into the process at the requested address. The GlobalAlloc function allocates the specified number of bytes from the heap. For more information on the global functions, refer to the MSDN Web site.

Both the UNIX and Windows file mapping solutions offer the capability of saving the contents in a permanent file.

Message Queues

The Microsoft Windows API does not support message queues as standard. If you want to use message queuing in your application, you should use Message Queuing (also known as MSMQ). Message Queuing is covered comprehensively in other Microsoft documentation and is therefore only briefly described here.

Message Queuing technology enables applications running at different times to communicate across heterogeneous networks and systems that may be temporarily offline. Applications send messages to queues and read messages from queues. Message Queuing provides guaranteed message delivery, efficient routing, security, and priority-based messaging. It can be used to implement solutions for both asynchronous and synchronous scenarios that require high performance.

Note Additional information on Message Queuing is available at https://www.microsoft.com/windows2000/technologies/communications/msmq/default.mspx.

Networking

The primary networking protocol for UNIX and Windows is TCP/IP. The standard programming API for TCP/IP is called sockets. The Windows implementation of sockets is Winsock 2 (formerly known as Windows Sockets). Winsock conforms well to the Berkeley implementation, even at the API level. Most of the functions are the same, but slight differences in parameter lists and return values do exist. RPC is another networking concept. Microsoft RPC provides RPC mechanisms for Windows-based applications. The following sections describe sockets and RPC mechanisms in UNIX and Windows. With this knowledge, you will be able to analyze UNIX networking applications and identify the appropriate porting approach for corresponding sockets and RPC implementations in the Windows environment.

TCP/IP and Sockets

Winsock 2 uses the sockets paradigm that was first popularized by Berkeley Software Distribution (BSD) UNIX. It was later adapted for Microsoft Windows in Winsock 1.1. One of the primary goals of Winsock has been to provide a protocol-independent interface that is fully capable of supporting emerging networking capabilities, such as real-time multimedia communications.

Winsock is an interface, not a protocol. As an interface, it is used to discover and use the communications capabilities of any number of underlying transport protocols. Because it is not a protocol, it does not in any way affect the bits on the wire and does not need to be used on both ends of a communications link.

Winsock programming previously centered on TCP/IP. Some of the programming practices that worked with TCP/IP do not work with every protocol. As a result, the Winsock API added new functions that were necessary to handle several protocols.

Winsock has changed its architecture to provide easier access to multiple transport protocols. Following the Windows Open System Architecture (WOSA) model, Winsock now defines a standard service provider interface (SPI) between the API with its functions exported from Ws2_32.dll and the protocol stacks. Consequently, Winsock support is not limited to TCP/IP protocol stacks as is the case for Windows Sockets 1.1. More information is available at the discussion on "Windows Sockets 2 Architecture" in Microsoft Visual Studio® .NET 2003.

There are new challenges in developing Winsock 2 applications. When sockets only supported TCP/IP, a developer could create an application that supported only two socket types: connectionless and connection-oriented. Connectionless protocols use SOCK_DGRAM sockets, and connection-oriented protocols use SOCK_STREAM sockets.

Because of the many new socket types introduced, developers can no longer rely on the socket type to describe all the essential attributes of a transport protocol.

Sockets are not a part of the Windows API. You will need to consult the Platform SDK for detailed information about the Winsock APIs. The help for the Platform SDK contains complete samples that demonstrate how to implement socket-based client and server applications. For an in-depth comparison between UNIX sockets and Winsock, refer to the discussion of "Porting Socket Applications to Winsock" in the Microsoft Platform SDK.

Note More information is available in the “Write Scalable Winsock Apps Using Completion Ports” MSDN article at https://msdn.microsoft.com/library/default.asp?url=/msdnmag/issues/1000/Winsock/toc.asp.

Remote Procedure Calls

In UNIX, routines that allow C programs to make remote procedure calls (RPCs) are in the rpc/rpc.h library.

In Windows, RPC for the C and C++ programming languages is provided by the Microsoft RPC. Microsoft RPC is compatible with Open Software Foundation (OSF) Distributed Computing Environment (DCE) RPC. Microsoft RPC differs from DCE RPC in the location service, which Microsoft RPC does not offer. When you have located an RPC service, you have binary compatibility. Microsoft RPC fully supports 64-bit Windows. Using RPC, developers can transparently communicate between different types of processes. RPC automatically manages process differences behind the scenes.

The process for creating a client/server application using Microsoft RPC is:

  1. Develop the interface.

  2. Develop the server that implements the interface.

  3. Develop the client that uses the interface.

All interfaces for programs using RPC must be defined in Microsoft Interface Definition Language (MIDL) and compiled with the MIDL compiler.

Windows Server 2003 Features

RPC includes a collection of new capabilities and improvements in Microsoft Windows Server™ 2003. Some of these capabilities are available in Windows XP as well, but are new for the server family of Windows.

The following capabilities are new in Windows Server 2003:

  • NDR64. NDR64 is a new transfer syntax optimized for 64-bit environments.

  • RPC over HTTP. RPC now enables its clients to securely and efficiently connect across the Internet to RPC server programs and execute remote procedure calls. More information is available in “Remote Procedure Calls Using RPC over HTTP” on the MSDN Web site.

  • Improved troubleshooting. RPC has improved its troubleshooting capabilities and extended error information. Refer to “Obtaining Extended RPC Error Information” on the MSDN Web site.

Other new capabilities include better configuration using RPC NetShell extensions, improved SDK samples such as FileRep, and support for RPC verifier to automatically check for errors in RPC code.

Note Guidance can be found on the MSDN Web site at https://msdn.microsoft.com/library/default.asp?url=/library/en-us/rpc/rpc/best_rpc_programming_practices.asp. Also, refer to the following URL at
https://msdn.microsoft.com/library/default.asp?url=/library/en-us/rpc/rpc/overviews.asp.

Miscellaneous Features

This section focuses on the miscellaneous infrastructure areas of the UNIX and Windows environments, listed as follows:

  • Shells and scripting

  • Scripting languages

  • Daemons versus services

  • Middleware

These topics give you information regarding how you can replace these specific implementations in your UNIX application with the equivalent features of the Windows environment.

Shells and Scripting

A shell is a command-line interpreter that accepts typed commands from a user and executes them. In addition to executing programs, shells usually support advanced features such as the capability to recall recent commands and a built-in scripting language for writing programs.

Programs written through the programming features of a shell are called shell scripts. In addition to scripts written through the use of shells, there are also languages specifically designed for writing scripts. As with shell scripts, these scripting languages are interpreted.

The use of scripting languages leads to rapid development, often with relaxed syntax checking, but slower performance. Windows and UNIX support a number of shells and scripting languages, some of which are common to both operating systems.

Command-Line Shells

On the Windows platform, Cmd.exe is the command prompt, or the shell. With the command prompt, a user can run programs or scripts and invoke applications. The command prompt has a memory or buffer for recent commands, so the user can retrieve, run, and edit them using various techniques.

On UNIX, a number of standard shells provide the UNIX user interface. These shells include:

  • The Bourne shell (sh). This is the simplest shell, often set as the default. It can invoke programs and create pipes, but it has no command memory or advanced scripting capabilities.

  • The C shell (csh). This shell includes command memory and a scripting language similar to the C language. An Interix/UNIX version of the C shell comes with the Interix product.

  • The Korn shell (ksh). The Korn shell also features command memory and a built-in language for creating script files. The Korn shell is based on the Bourne shell but includes additional features, such as job control, command-line editing, functions, and aliases. Interix/UNIX versions of the Korn shell are delivered with Windows Services for UNIX and Interix products.

Scripting Languages

The following sections explain the scripting languages and scripting-language support provided in Windows and UNIX.

  • Windows Script Host. Windows Script Host (WSH) is a language-independent environment for running scripts and is often used to automate administrative tasks and logon scripts. WSH provides objects and services for scripts, establishes security, and invokes the appropriate script engine, depending on script language. Objects and services supplied allow the script to perform such tasks as displaying messages on the screen, creating objects, accessing network resources, and modifying environment variables and registry keys. WSH natively supports Microsoft Visual Basic® Scripting Edition (VBScript) and Microsoft JScript®. Other languages that are available for this environment are Perl, REXX, and Python. WSH is built in to all versions of Windows after Microsoft Windows® 95. It can also be downloaded or upgraded from the Microsoft Web site.

  • Perl. Perl is an acronym for Practical Extraction and Report Language. It is an interpreted language that was originally designed for UNIX, but it has since been ported to many platforms. Perl provides a cross-platform scripting environment that developers can use to write scripts that can be run on both Windows and UNIX. Perl is effective for string manipulation. Although Perl is not delivered with Windows, there are many versions of Perl available that are designed to run on Windows. Perl also comes with Windows Services for UNIX.

  • REXX. REXX is an acronym for Restructured Extended Executor Language and was originally developed by IBM United Kingdom Laboratories. It is a procedural language that is designed for application programs to use as a macro or scripting language. Although REXX can issue commands to its host environment and can call programs and functions written in other languages, it is designed to be independent of a specific operating system. Versions of REXX are available for both UNIX and Windows.

  • Python. Python, just like Perl, is an interpreted language. Many of its features are similar to Perl, but its programming structure and syntax are clearer, thus making Python code easier to read and maintain. Although it was designed for UNIX, it is now widely available on other platforms, including Windows. Python is object-oriented and includes dynamic data structures and dynamic typing. Python is ideal for rapid software development where maintainable code is important. Python is not shipped with Windows, but it can be downloaded from the Python Web site at https://www.python.org/download/.

  • Tcl/Tk. Tcl/Tk is yet another interpreted language. Like Perl, it is effective for string manipulation and is available across UNIX and Windows platforms. Tcl/Tk is particularly applicable to the development of cross-platform GUIs. Tcl/Tk is not shipped with Windows, but it can be downloaded from the Tcl/Tk Web site at https://www.tcl.tk.

Daemons vs. Services

A UNIX daemon is a process that runs in the background and does not require a user interface. A service application is the equivalent of a daemon on Windows. Normally, a daemon is started when the system is booted and runs without supervision until the system is shut down. Similarly, a Windows service can be started at boot time and run until system shutdown. However, the Service Control Manager (SCM) controls all services. To convert daemon code to run on Windows, you must add code to interface with SCM. This section briefs you on the Windows API functions to convert UNIX daemons to Windows services.

Unless the main function of the daemon is extremely simple, the best strategy is to rename it to something else, such as service_main. Then create a new main that contains code to install, uninstall, and run the service depending on command-line arguments.

To install the program as a service, the program must call OpenSCManager to get a handle to the SCM. It must then call CreateService, passing the SCM handle and several arguments, including the service name, display name, service type, and path to the executable, and identity the service uses to run. After the service is installed, the administrative tools services applet can be used to examine and modify many of these values.

Uninstalling the service is similar to installing it. Call OpenSCManager to get a handle to the SCM, and then call OpenService to get a handle to the service. If it is running, the service should be stopped by calling the ControlService function. Finally, call DeleteService, passing the service handle, and close the handles to clean up.

Running the daemon as a service is also fairly straightforward. The new main function sets up a SERVICE_TABLE_ENTRY structure that contains a name and a pointer to the main function of the service, which is the old main function renamed service_main. This structure is passed to the StartServiceCtrlDispatcher function, which does not return until the service stops. Note that more than one service entry can be present in the service entry structure, so a single executable can support more than one service. The definitions are the same as a main function; however, the arguments to the main function of the service are supplied by SCM and can be set through the services applet or the CreateService call.

The main function of the service needs new code to call the SetServiceStatus function, which keeps SCM informed of the status of the service during startup. If the SCM does not receive status updates within a specified time period, it assumes that the service has stopped running and logs an error. The SCM must also be given the address of a service control function that the SCM can use to request actions such as requesting the service to stop. Call the RegisterServiceCtrlHandler or RegisterServiceCtrlHandlerEx function to set this address. When the service is fully initialized, it should call SetServiceStatus with the SERVICE_RUNNING status to complete the startup sequence.

Note For sample service programs and details of the service functions, refer to the MSDN Web site at https://msdn.microsoft.com/.

Middleware

This section compares the various middleware solutions available for UNIX-based and Windows-based applications. With the information provided in this section, you will be able to identify the various middleware technologies available on UNIX and the alternative Windows technologies to use in migrating UNIX middleware to the Windows environment.

OLTP Systems

Online transaction processing (OLTP) systems have been implemented in UNIX environments for many years. These systems perform such functions as resource management, threading, and distributed transaction management. OLTP systems typically provide support for multiple languages and development environments.

Common OLTP systems include:

  • Tuxedo from BEA Systems.

  • TOP END from NCR Corporation.

  • Encina for DCE (distributed computing environment) from Transarc.

Although OLTP was originally developed for UNIX, many OLTP systems also have Windows versions. Additionally, gateways exist to integrate systems that use different transaction monitors—for example, the Tuxedo gateway to Top End. OLTP systems currently face the challenge of integrating with Web and e-business systems. Many OLTP systems provide a bridge to the Java programming language and provide gateways to Common Object Request Broker Architecture (CORBA) and COM.

When considering transaction and resource management during a UNIX migration, developers should remember that OLTP systems provide many of the same features as COM+. As with most cross-platform products, OLTP monitors achieve these features by introducing new APIs to the development environment. Introducing COM+ for transaction and resource management during a migration can lessen this type of dependency.

Queuing Systems

Message queuing is provided as a feature in AT&T System V UNIX and can be achieved through sockets in Berkeley UNIX versions. These types of memory queues are most often used for interprocess communications and do not meet the requirements for persistent store and forward messaging.

To help meet these requirements in UNIX, versions of MQSeries from IBM and MessageQ (formally the DEC MessageQ) from BEA Systems are available. A reliable and resilient store-and-forward message queue provides a key building block for enterprise integration and highly available, loosely coupled systems.

Microsoft provides similar functionality for Windows through Message Queuing. IBM and BEA Systems also provide versions of their queuing systems for Windows. Gateway offers products that bridge the various queuing systems. One reason for migrating to Windows may be the need to integrate with commercial off-the-shelf applications. The queuing system for such a migration would need to provide an API that easily integrates into these applications. For example, Message Queuing provides for a COM Automation Interface API and .NET classes.

Component-based Development in Windows

The Windows platform offers developers a wide range of component-based development tools and technologies. One of these is the Component Object Model (COM).

Component Object Model

COM is the first component-based development technology from Microsoft. Developers can use COM to develop component-based software by exploiting a set of well-defined development techniques and run-time services. By adhering to the COM development model and by using one of the many COM-aware development environments, developers can easily build component-based software that is capable of interacting with other components developed by different organizations, potentially in different development languages.

Although many of the required development techniques—such as how functionality should be exposed through interfaces—are complex, the development environments available on the Windows platform mask this complexity. One of the most popular development environments is Visual Basic.

Some of the key features of the COM programming model are as follows:

  • COM objects expose functionality through well-defined interfaces, the binary format of which is defined by the COM specification. This functionality matches the classic C++ virtual function table [v-table] layout in memory.

  • An interface consists of a set of methods. However, most development environments also allow properties to be exposed at the interface level through a pair of property-get and property-set methods.

  • COM supports component versioning.

  • COM components can be hosted in-process (through DLLs), out-of-process (through executable files), or in executable files on remote computers.

  • All COM components and COM interfaces on a particular computer are logged centrally in the Windows registry, which is a hierarchical configuration database for the Windows platform.

The Windows registry contains such information as system hardware details, hardware and system configuration, and details of applications installed on the system. For COM, the registry stores a globally unique identifier (GUID) to identify each component class and interface installed. GUIDs are 128-bit integers that are guaranteed to be unique. COM uses this information to determine which component class to create when an application make a request for an object (component) to be instantiated.

Each component also has a user-friendly name known as a ProgID, or programmatic identifier, that is created by the component vendor. The ProgID is not guaranteed to be unique. The recommended format for a ProgID is vendor.component.version, where vendor and component are alphanumeric names.

When an application needs to use an object, it starts by calling the CoCreateInstance COM function to create the component. This function takes the registered GUID for the object class (CLSID) as an argument. If the developer chooses to use the user-friendly ProgID instead, the application first     calls a function to get the CLSID from the ProgID. The application may also pass the initial interface GUID to CoCreateInstance, or it may pass a null entry to receive the default interface. COM finds the server for the class, loads the class into memory if necessary, and marshals the call if the server is in another process or across the network.

After a COM component is created, the application can query a particular interface and use the interface to perform work. Because the interfaces are identified by GUIDs just as the components are, the QueryInterface call takes the GUID as an argument and either returns the interface requested or returns a null entry if the interface is not implemented by the class.

Note Additional information about COM is available at https://www.microsoft.com/com.

Download

Get the UNIX Custom Application Migration Guide

Update Notifications

Sign up to learn about updates and new releases

Feedback

Send us your comments or suggestions