How SerenityOS declares ssize_t

This post explores one of my favorite hacks in SerenityOS. I don’t recommend doing this in your codebase, but it has worked for us so far. :^)


Background

size_t and ssize_t are common types used in many POSIX APIs. According to POSIX, they are used as follows:

  • size_t: Used for sizes of objects.
  • ssize_t: Used for a count of bytes or an error indication.

In practice, ssize_t is essentially a “signed size_t”.

Since we’re building the whole operating system, including the standard C library ourselves, we’re also responsible for declaring all the common system types, including size_t and ssize_t.

How we declare them

To declare size_t, we leverage the C preprocessor’s predefined __SIZE_TYPE__ macro:

typedef __SIZE_TYPE__ size_t;

However, there is no __SSIZE_TYPE__ macro for ssize_t, so I decided to get a little creative:

#define unsigned signed
typedef __SIZE_TYPE__ ssize_t;
#undef unsigned

Here’s what’s happening: The C preprocessor expands “__SIZE_TYPE__” to “unsigned long” or something similar. We trick it by temporarily defining a macro that replaces “unsigned” with “signed”, and so ssize_t is declared as a signed version of whatever the size_t type is!

How others declare them

Other C libraries typically use more careful techniques, such as wrapping the declarations in architecture-specific #ifdefs:

#ifdef __i386__
typedef uint32_t size_t;
typedef int32_t ssize_t;
#endif

#ifdef __x86_64__
typedef uint64_t size_t;
typedef int64_t ssize_t;
#endif

That’s obviously a better approach if you’re building with compilers that don’t predefine __SIZE_TYPE__, or in an environment where unsigned has already been redefined.

In our case, we haven’t had any issues with our approach yet, so I’m inclined to hold on to the hack as it’s just so cute. :^)

Written on April 4, 2023