161 lines
11 KiB
Markdown
161 lines
11 KiB
Markdown
# Portability
|
|
|
|
Portable [software](software.md) is software that is easy to [port](port.md) to (make run on) other [platforms](platform.md). Platforms here mean anything that serves as an environment enabling software to run, i.e. [hardware](hardware.md) platforms ([CPUs](cpu.md), [ISAs](isa.md), game consoles, ...), different [operating systems](os.md) vs [bare metal](bare_metal.md), [fantasy consoles](fantasy_console.md) etc. **Portability is an extremely important attribute of [good software](lrs.md)** as it allows us to write the program once and then run it on many different computers with little effort -- without portability we'd be constantly busy rewriting old programs to run on new computers, portability allows us to free our programs from being tied to specific computers and exist abstractly and independently and so become [future proof](future_proof.md). Examples of highly portable programs include [Anarch](anarch.md), [Simon Tatham's Portable Puzzle Collection](stppc.md), [sbase](sbase.md) (suckless) implementation of Unix tools such as [cat](cat.md) and [cmp](cmp.md) etc.
|
|
|
|
**Portability is different from mere [multiplatformness](multiplatform.md)**: multiplatform software simply runs on more than one platform without necessarily being designed with high portability in mind; portable software on the other hand possesses the inherent attribute of being designed so that very little effort is required to make it run on wide range of general platforms. Multiplatformness can be achieved cheaply by using a [bloated](bloat.md) framework such as the Godot engine or [QT](qt.md) framework, however that will not achieve portability; on the contrary it will hurt portability. Portability is achieved through good and careful design, efficient code and avoiding [dependencies](dependency.md) and [bloat](bloat.md).
|
|
|
|
In connection to software the word *portable* also has one other meaning used mainly in context of [Windows](windows.md) programs: it is sometimes used for a binary executable program that can be run without installing (i.e. it can be carried around and ran from a USB drive etc.). However we'll stick to the previously defined meaning.
|
|
|
|
## How To Make Portable Programs
|
|
|
|
In short: use [abstraction](abstraction.md) to not get tied to any specific platform (separate [frontend](frontend.md) and [backend](backend.md)), [keep it simple](kiss.md), minimize [dependencies](dependency.md) (minimize use of [libraries](library.md) and requiring hardware such as [floating point](float.md) unit or a [GPU](gpu.md), have [fallbacks](fallback.md)), write efficient, [simple](kiss.md) code (lower hardware demands will support more platforms), avoid platform-specific features (don't write in [assembly](assembly.md) as that's specific to each CPU, don't directly use [Linux](linux.md) [syscalls](syscall.md) as these are specific to Linux etc.).
|
|
|
|
Remember, portability is about making it easy for a programmer to take your program and make it run elsewhere, so portability is about constantly putting oneself in the shoes of someone else with a very different computer and asking questions such as "how hard will it be to make this work if this library isn't available?". Even things that are supposed or commonly expected to be present on all platforms, such as a file system or a raster screen, may not be present on some computers -- always remember this.
|
|
|
|
**Do NOT use big frameworks/engines** -- it is one of the greatest misconceptions among many inexperienced programmers to think portable software is created with big frameworks, such as the [Godot](godot.md) engine or the [QT](qt.md) framework, which can "single click" export/deploy software to different platforms. This will merely achieve creating a badly [bloated](bloat.md) multiplatform program that's completely dependent on the framework itself which drags along hundreds of [dependencies](dependency.md) and wastes computing resources (RAM, CPU, storage, ...) which are all factors directly contradicting portability. If you for example create a snake game in Godot, you won't be able to port it to [embedded](embedded.md) devices or devices without an operating system even though the snake game itself is simple enough to run on such devices -- the game drags along the whole Godot engine which is so huge, complex and hardware demanding that it prevents the simple game from running on simple hardware.
|
|
|
|
**The same goes for languages and [libraries](library.md)**: do NOT use big/bloated languages such as [Python](python.md), [Java](java.md) or [JavaScript](javascript.md) -- your program would immediately become dependent on a hugely complex ecosystem of such language. For portability you should basically **only write in [C](c.md)** (the best established, time tested, relatively simple language supported basically by every platform) or in [C++](cpp.md) at worst, and even with these languages do NOT use the newer standards as these hugely limit the number of compliant compilers that will be able to compile your program. The best is to write in C89 or C99 standard of C. **Minimize the number of libraries you use**, even if it is the standard library of your language -- not all compilers fully adhere to standards and some don't have the standard library even if the should.
|
|
|
|
**Always make your own thin [I/O](io.md) abstraction, [decouple](coupling.md) your I/O libraries, separate [frontend](frontend.md) and [backend](backend.md)**. This is one of the most basic and most important things to do. Why? Well unless you're writing a library, you will need to use I/O (write out messages, draw to screen, create [GUI](gui.md), read keyboard commands, read from files, read from network, ...) so you will NEED to use some library for this (C [stdlib](stdlib.md), [SDL](sdl.md), OS [syscalls](syscall.md), [Xlib](xlib.md), ...) but you absolutely DON'T WANT this library to become a hard [dependency](dependency.md) of your program because if your program depends let's say on SDL, you won't be able to make your program run on platforms that don't have SDL. So the situation is that you HAVE TO use some I/O library but you don't want to become dependent on it.
|
|
|
|
The way to solve this is to create your own small I/O abstraction in your project, i.e. your own functions (such as `drawPixel`, `writeMessage`, `keyPressed`, `playSound`, `readFrile` etc.) for performing I/O, which you will use inside your main program. These functions will be defined in a small file which will basically be your own small I/O library just for your program. The functions you define there will then internally use functions of whatever underlying I/O system you choose to use at the time as your [frontend](frontend.md) (SDL, Xlib, SFML, ...); the important thing is that your main program code won't itself depend on the underlying system, it will only depend on your I/O abstraction, your own functions. Your custom I/O functions will depend on the underlying I/O system but in a way that's very easy to change -- let's say that your `keyPressed` function internally uses SDL's `SDL_GetKeyboardState` to read keyboard state. If you want to switch from using SDL to using a different frontend, you will only have to change the code in one place: in your I/O abstraction code, i.e. inside your `keyPressed` function. E.g. if you switch from SDL to SFML, you will just delete the code inside your `keyPressed` function and put in another code that uses SFML functions to read keyboard (e.g. the `isKeyPressed` attribute), and your whole code will instantly just work on SFML. In fact you can have multiple implementations of your functions and allow switching of different backends freely -- just as it is possible to compile a [C](c.md) program with any C compiler, you can make it possible to compile your program with any I/O frontend. If you used SDL's specific functions in your main code, you would have to completely rewrite your whole codebase if you wanted to switch away from SDL -- for this reason your main code must never directly touch the underlying I/O system, it must only do so through your I/O abstraction. Of course these principles may apply to any other thing that requires use of external libraries, not just I/O.
|
|
|
|
This is all demonstrated by [LRS](lrs.md) programs such as [Anarch](anarch.md) or [SAF](saf.md), you can take a look at their code to see how it all works.
|
|
|
|
Anyway the following is a simple [C](c.md) code to demonstrate the abstraction from an I/O system -- it draws a dithered rectangle to the screen and waits until the user pressed the `q` key, then ends. The main code is written independently of any I/O system and can use either C [stdlib](stdlib.md) (*stdio*, draws the rectangle to terminal with ASCII characters) or SDL2 (draws the rectangle to actual window) as its frontend -- of course more frontends (e.g. one using Xlib or SFML) can be added easily, this is left as an exercise :)
|
|
|
|
```
|
|
#define SCREEN_W 80
|
|
#define SCREEN_H 30
|
|
|
|
// our I/O abstraction:
|
|
void ioInit(void); // init our I/O
|
|
void ioEnd(void); // destroy our I/O
|
|
void drawPixel(int x, int y, int white);
|
|
void showImage(void);
|
|
int isKeyPressed(char key);
|
|
|
|
// our main program code:
|
|
int main(void)
|
|
{
|
|
ioInit();
|
|
|
|
for (int y = 3; y < 20; ++y) // draw dithered rectangle
|
|
for (int x = 30; x < 60; ++x)
|
|
drawPixel(x,y,x % 2 == y % 2);
|
|
|
|
showImage();
|
|
|
|
while (!isKeyPressed('q')); // wait for pressing 'q'
|
|
|
|
ioEnd();
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*---------------------------------------------------
|
|
implementation of our I/O abstraction for different
|
|
frontends: */
|
|
|
|
#ifdef FRONTEND_STDLIB // C stdio terminal frontend
|
|
#include <stdio.h>
|
|
char screen[SCREEN_W * SCREEN_H];
|
|
|
|
void ioInit(void)
|
|
{
|
|
// clear screen:
|
|
for (int i = 0; i < SCREEN_W * SCREEN_H; ++i)
|
|
screen[i] = 0;
|
|
}
|
|
|
|
void ioEnd(void) { } // nothing needed here
|
|
|
|
void drawPixel(int x, int y, int white)
|
|
{
|
|
screen[y * SCREEN_W + x] = white != 0;
|
|
}
|
|
|
|
void showImage(void)
|
|
{
|
|
for (int i = 0; i < SCREEN_W * SCREEN_H; ++i)
|
|
{
|
|
if (i % SCREEN_W == 0)
|
|
putchar('\n');
|
|
|
|
putchar(screen[i] ? '#' : '.');
|
|
}
|
|
|
|
putchar('\n');
|
|
}
|
|
|
|
int isKeyPressed(char key)
|
|
{
|
|
return getchar() == key;
|
|
}
|
|
#elif defined(FRONTEND_SDL) // SDL2 frontend
|
|
#include <SDL2/SDL.h>
|
|
unsigned char screen[SCREEN_W * SCREEN_H];
|
|
SDL_Window *window;
|
|
SDL_Renderer *renderer;
|
|
SDL_Texture *texture;
|
|
|
|
void ioInit(void)
|
|
{
|
|
for (int i = 0; i < SCREEN_W * SCREEN_H; ++i)
|
|
screen[i] = 0;
|
|
|
|
SDL_Init(0);
|
|
|
|
window = SDL_CreateWindow("sdl",SDL_WINDOWPOS_UNDEFINED,
|
|
SDL_WINDOWPOS_UNDEFINED,SCREEN_W,SCREEN_H,SDL_WINDOW_SHOWN);
|
|
|
|
renderer = SDL_CreateRenderer(window,-1,0);
|
|
|
|
texture = SDL_CreateTexture(renderer,SDL_PIXELFORMAT_RGB332,
|
|
SDL_TEXTUREACCESS_STATIC,SCREEN_W,SCREEN_H);
|
|
}
|
|
|
|
void ioEnd(void)
|
|
{
|
|
SDL_DestroyTexture(texture);
|
|
SDL_DestroyRenderer(renderer);
|
|
SDL_DestroyWindow(window);
|
|
}
|
|
|
|
void drawPixel(int x, int y, int white)
|
|
{
|
|
screen[y * SCREEN_W + x] = (white != 0) * 255;
|
|
}
|
|
|
|
void showImage(void)
|
|
{
|
|
SDL_UpdateTexture(texture,NULL,screen,SCREEN_W);
|
|
|
|
SDL_RenderClear(renderer);
|
|
SDL_RenderCopy(renderer,texture,NULL,NULL);
|
|
SDL_RenderPresent(renderer);
|
|
}
|
|
|
|
int isKeyPressed(char key)
|
|
{
|
|
SDL_PumpEvents();
|
|
const unsigned char *keyboard = SDL_GetKeyboardState(NULL);
|
|
|
|
return keyboard[SDL_SCANCODE_A + (key - 'a')];
|
|
}
|
|
#endif
|
|
```
|
|
|
|
If you compile this code as
|
|
|
|
```
|
|
gcc -DFRONTEND_STDLIB main.c
|
|
```
|
|
|
|
You'll get the stdlib version. If you compile it as
|
|
|
|
```
|
|
gcc -DFRONTEND_SDL -lSDL2 main.c
|
|
```
|
|
|
|
You'll get the SDL version. |