Cross-compilation with C

Cross-compilation with C

How to create small MacOS, Linux, and Windows binaries with C and (practically) without dependencies

Brainist's photo
Brainist
·Sep 19, 2021·

4 min read

First, a bit of context

I use Debian, Windows, and macOS as my operating systems. To process data from various sources, I maintain and create data workflow pipelines in these systems.

Simulations are one example of a task that I frequently face. I need to code in any language and then upload it to a more powerful workstation/server or update it to the computer where the data is stored. However, I don't always have the permission to install all of the dependencies I require, and in some cases, I can't even install a compiler without additional permissions (in some cases, that means goodbye to good libraries in R).

It is always a pain to distribute source code and binaries across the platforms on which I work. Even though in some of these cases, I was given Python (or NodeJS). But, some of those simulations are small enough that I can code them in C and speed up their execution.

In those cases, I'd like to write something in C, compile it, upload the binary to the server, and execute it without any hassle with something as simple as

chmod +x simulations
./simulations

This is possible with some toolchains. But there's still one issue: I can't share binaries with macOS!

For the other cases, I either need to create separate environments for each target system or using some pre-built Docker containers.

Requirements

  • No large build dependencies (<10MB). No VM, No HUGE toolchains (MinGW, Docker, and so)
  • I can live with less-than-optimal binaries as long as I don't have to maintain a huge infrastructure of dependencies.
  • Easy to compile in Windows/Linux and deliver on all platforms
  • Debugging not required (I can use GCC/GDB)

Solution: SmallerC + YASM

SmallerC is a "simple and small single-pass C compiler" that generates 32-bit instructions using the C programming language (almost complete standard C90). It can be combined with several assemblers (NASM, YASM, NASM) to provide a final executable. Because of this decoupling, we can use a cross-platform assembler like YASM to compile executables for different platforms.

Configuring SmallerC

  1. Clone the GitHub repository SmallerC, or download the ZIP source code. I'll do it ~/.smlrcc, but you can use your favorite directory.

In order to create binary files in Windows, the files must be combined. Some of the compiler's low-level instructions can cause anti-malware software to report false positives.

If you don't trust the code, you can always read, check, and compile it yourself (just type ./configure && make in the root directory and that's all).

  1. Set the environment variable SMLRC. In Linux/macOS, you can do it by adding this at the end of ~/.profile:

    export SMLRC=~/.smlrcc/v0100
    
  2. There are three folders with the binaries ~/.smlrcc/v0100/binw, ~/.smlrcc/v0100/binl or ~/.smlrcc/v0100/binm in your system PATH. For instance, in macOS you can do it by adding this in ~/.profile:

    export PATH="$PATH:~/.smlrcc/v0100/bin"
    

Configuring YASM

  1. Install YASM:
    • Windows: Download here or using MSYS2 (recommended):
pacman -S yasm
  • Debian/Ubuntu:
sudo apt install yasm
sudo port install yasm

Cross-compiled Hello World

Now that everything was configured. Let's make our first cross-compiled file.

Let us create a sample file sample.c:

#include <stdio.h>

int main(int argc, char** argv){
  printf("Hello world!");
  return 0;
}

Now, let's generate the source codes for:

  • Windows:
smlrcc sample.c -win -o sample-win.exe
  • Linux:
smlrcc sample.c -Linux -o sample-lin
  • MacOS:
smlrcc sample.c -macos -o sample-mac

To compare, we can also run GCC -O2 -o GCC-sample-default sample.c:

$ ls -Alh | awk '{print $5, $9}'
357K GCC-sample-default
20K sample-lin
17K sample-mac
26K sample-win.exe
99 sample.c

Some final thoughts

  • PROS:

    • Mature? project: 6 years of development.
    • Awesome Portability: macOS from Windows? I feel almost like using electron-builder.
    • Small binaries. Executables are small by default (not optimal, but small after all) with minimal dependencies.
    • Well-written and small codebase. It's easy to check and fix small bugs or features. It also helps me to understand the assembler code generation process.
  • CONS:

    • Some subset of C90 is not supported. More details here
    • No debugger. This is the part that I missed the most. But, I understand it is difficult to write. In any case, we can compile a debug version with GCC -O0 -g, and debug it with gdb.
    • Not ready for production. Don't get me wrong: I think SmallerC is amazing. It's stable for internal use, but I'm not sure if it's ideal for commercial products that are completely reliant on it. In these situations, having a large toolchain may be preferable.
    • It's not suitable for everyone, as every user can have different experiences. In many cases, the source code will need minor changes.

Links

 
Share this