Secure Oldies II: Refactoring and Make it Compile on macOS and Linux.

Secure Oldies II: Refactoring and Make it Compile on macOS and Linux.

Windows and Unix is not THAT different

This second article is an continuation of my first article about building a simple HTTP client which runs on Windows 2000 onwards.

Before going backward to the like of Windows 9x, Me, and NT 4.0 we're going lateral first. We'd try to make our code works on latest MacOS and Linux.

The Tale of Portability

A portable code is a code that can be compiled and run on many different platforms. For now, our code only runs on Windows only. Specifically Windows NT 5.0 onwards. As I've said on my first article on this series, Sockets API is everywhere including macOS and Linux. So let's make it happen.

Windows implementations of Winsock is largely based on BSD socket. However, there are some difference between it and BSD socket usually used in Unix or Unix-like operating systems like Linux and macOS.

  1. The most visible is that the existence of WSAStartup() and WSACleanup() function which is mandatory on Windows.
  2. Windows uses closesocket() to close a socket rather than typical close() function available on POSIX compatible operating systems.
  3. SOCKET on Windows is not file descriptor, it's private data structure specific to Windows and must be used by WinSock functions only and is not interchangeable with file descriptors with int data type.
  4. WSAGetLastError() is used to get last error on Winsock code rather than errno typical in Unix systems.

Because of those differences, we'd need to isolate platform specific codes.

Isolating Platform specific codes.

First of all we'd need to refactor out the header inclusion between Windows and Unix Headers. For that I'd create a file with #ifdef for each platform. I'll name it platform.h. The contents of this header is only platform specific header inclusion.

#ifndef OTLS_PLATFORM_H
#define OTLS_PLATFORM_H 1 

#if defined (WIN32)
#define WIN32_LEAN_AND_MEAN // (1)
#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#elif defined(__linux__) || defined (__unix__) || defined (__APPLE__) 
#define closesocket close // (2)
#define SOCKET int
#define INVALID_SOCKET -1
#define SOCKET_ERROR -1
#include <errno.h>
#include <netdb.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#else 
#error "Unsupported platform" 
#endif

// .. more code here ..

#endif

We add needed headers and add #ifdef for WIN32 :

  1. This part will be compiled if we're targeting Windows
  2. This part will be compiled if we're targeting Unix/Linux/macOS
    • We redefine SOCKET to int because in Unix, socket is file descriptor.
    • INVALID_SOCKET and SOCKET_ERROR will be -1 as this is the value on POSIX API for failure.
    • <errno.h> provides errno variable which similar to WSAGetLastError().
    • <netdb.h> provides struct in_addr and function gethostbyname().
    • <netinet/in.h> provides struct sockaddr_in and constants for internet protocol families.
    • <arpa/inet.h> provides inet_ntoa function which we're using to convert an address to human-readable address.
    • <unistd.h> provides close() function.

Also we're defining startup, cleanup and error code function which will call WSAStartup, WSACleanup in Windows and will be no-op in Unix. And will call WSAGetLastError in Windows and return errno in Unix.

static inline int init_socket() {
#ifdef WIN32
  WSADATA wsa;
  return WSAStartup(MAKEWORD(2, 0), &wsa);
#else 
  return 0;
#endif
}

static inline int cleanup_socket() {
#ifdef WIN32
  WSADATA wsa;
  return WSAStartup(MAKEWORD(2, 0), &wsa);
#else 
  return 0;
#endif

}

static inline int get_last_socket_error() {
#ifdef WIN32
  return WSAGetLastError(); 
#else
  return errno;
#endif
}

With code isolated and some redefined to fit the target platform, we can just replace any occurence of WSAStartup to init_socket() WSACleanup to cleanup_socket() and WSAGetLastError to get_last_socket_error(). And the include part will be like so:

#include <stdio.h>
#include <stdint.h>
#include <string.h>

#include "config.h"
#include "platform.h" // include this 

// ... more code below

And also, you may see there's new other header config.h. I move every configuration to this file like MTU and maximum buffer size.

#ifndef OTLS_CONFIG_H 
#define OTLS_CONFIG_H 1 

#define MTU 1500 
#define BUF_MAX_SIZE (8 * 2014)

#endif /* OTLS_CONFIG_H */

With this sorted out, all we need to do is just revise CMakeLists.txt a little bit so it only link to ws2_32.dll on Windows and not doing anything else on Linux or macOS.

if (WIN32)
  target_link_libraries(binfetch ws2_32)
endif (WIN32)

Rebuild and Run

The step on macOS and linux is the same. Prepare a directory for building and then use these commands.

cd build

cmake ..

make

./binfetch

The command is short because I believe you're building the software natively. Because the code becomes complex. I've prepared the source repo for you to try.

Here's the result on macOS

macos.gif