>
>
>
Games! How they write code for SDL (+ i…

Anton Tretyakov
Articles: 5

Games! How they write code for SDL (+ interview with the creator)

Jack London once said, "You can't wait for inspiration. You have to go after it with a club". When it comes to game development, the SDL project may well be such a club. How is it made, though?

If you've ever wanted to develop your own game, you've probably come across SDL. If you've ever wanted to learn how to draw things on your screen for free (no viruses), you've probably come across SDL. If you've ever needed to initialize OpenGL, you've definitely encountered SDL.

And if you haven't, we highly recommend that you learn about it! SDL is a cross-platform library for graphics, audio, inputs, and everything else you need to finally create your game!

Neverwinter Nights, Dwarf Fortress, Amnesia, VVVVVV (on which we have a similar article), and the great and powerful Teeworlds (where Yours Truly spent a considerable amount of time hoockin' and bazookin' around) are among the games that use SDL in one way or another.

The project has been around for decades — its original creator and the community are still actively developing it. So, why don't we take this great chance to learn something new? Let's take a flashlight and look under the hood of the project!

Moreover, this time we decided to experiment a bit and take a little interview with the project creator, which we invite you, dear reader, to find at the end of the article.

All right, enough wasting time, go-go-go!

We use the code examples throughout the article. The ellipsis characters "...." in the code were added by the author of the article.

You can find the source files on the official GitHub of libsdl. In addition, each fragment has a reference to a specific area in the code.

By the time this article is published, many errors will have already been fixed thanks to the issues we opened (for example, here and there). However, the links in the examples point exactly to the code you see in this article. Not only do we enjoy teasing developers but we also like making their projects a little bit better!

Trust but verify

How much, dear reader, do you trust the documentation of the standard C library? Although a more appropriate question for our opening example would be: how much do you trust yourself when you use one? What do you think is wrong with the following example?

File: SDL/src/stdlib/SDL_iconv.c (GitHub permalink)

char *SDL_iconv_string(const char *tocode, const char *fromcode,
                       const char *inbuf, size_t inbytesleft)
{
  SDL_iconv_t cd;
  ....

  cd = SDL_iconv_open(tocode, fromcode);
  if (cd == (SDL_iconv_t)-1) {
    /* See if we can recover here (fixes iconv on Solaris 11) */
    if (tocode == NULL || !*tocode) {
      tocode = "UTF-8";
    }
    if (fromcode == NULL || !*fromcode) {
      fromcode = "UTF-8";
    }
    cd = SDL_iconv_open(tocode, fromcode);
  }
  ....
}

"I might have answered," the attentive and keen reader may object, "if we were dealing with the standard iconv_open function. But there's a custom SDL_iconv_open implementation here!" That's an excellent observation! Here's how the function is implemented:

File: SDL/src/stdlib/SDL_iconv.c (GitHub permalink)

SDL_iconv_t SDL_iconv_open(const char *tocode, const char *fromcode)
{
  return (SDL_iconv_t)((uintptr_t)iconv_open(tocode, fromcode));
}

With that out of the way, let's look at the neighborhood. The code below illustrates that null pointers can be passed to the input in the tocode and fromcode arguments. In this case, they will go into iconv_open before the check.

When we look at the man page of this function from the GNU project, we don't see any mention of the fact that NULL can't be pushed into it. "Case solved!" our junior investigator would say, but you can't fool a seasoned detective! He really loves looking at the C standard library source code, because looking at the C standard library source code is fun! You too, behold!

Well... there is no pointer dereferencing, but a call to strlen without a NULL check is for sure present. We all know what strlen does in this case, and hardly anybody anywhere ever enjoyed it!

But if you, kind reader, thought we would stop at one implementation, let us hasten to change your mind — BSD and Musl behave the same way.

A harmless fragment, it would seem — even the check is there! But you should know your library dealer by sight! By the way, how often do you, dear reader, have the need to see your vendor's source code? Let's chat in the comments!

Well, the analyzer issued a concise message:

V595 The 'tocode' pointer was utilized before it was verified against nullptr. Check lines: 37, 789, 792. SDL/src/stdlib/SDL_iconv.c:792:1

Cargo cult

What about a little confession? Dost thou have those little rituals that are to be followed, elsewise thou labor in vain? Do you pet a rubber duck or save a source file three times in a row? The following is an example of such behavior:

File: SDL/src/events/SDL_mouse.c (GitHub permalink)

SDL_Cursor *SDL_GetCursor(void)
{
  SDL_Mouse *mouse = SDL_GetMouse();
  if (mouse == NULL) {
    return NULL;
  }
  return mouse->cur_cursor;
}

"What's wrong now?" the reader may cry out. The pointer is requested, the pointer is checked — everything is fair and square! We understand, after all, as we have written above, this is exactly the desired behavior! However, I urge you to hold your outrage and see where NULL comes from.

File: SDL/src/events/SDL_mouse.c (GitHub permalink)

SDL_Mouse *SDL_GetMouse(void)
{
  return &SDL_mouse;
}

Well, well, what kind of variable do we have here? Let's find out!

File: SDL/src/events/SDL_mouse.c (GitHub permalink)

static SDL_Mouse SDL_mouse;

It's a global variable! This means that NULL has no place here, as a global variable is always located in memory at a specific address.

What can the analyzer do other than issue another concise message:

V547 Expression 'mouse == NULL' is always false. SDL/src/events/SDL_mouse.c:1376:1

P.S. With the above in mind, we can rewrite the body of the function as follows:

return SDL_GetMouse()->cur_cursor;

Some vigilant readers may have noticed that if we replace a static variable with an allocation, the missing NULL check will play a cruel trick on us. Rest assured, any static analyzer worth its salt can tell you that the NULL check is required. This saves both the abstraction layer and the programmer's nerves.

We ask the readers who rightly point out that "every pointer returned by a function must be checked no matter what" to be patient and wait a little. I promise, we'll come back to this topic later with another fun example!

Do or do not

Did you know that the number of fingers on Master Yoda's hand changes from movie to movie? And now, back to the C language! There is a certain structure:

File: /SDL/src/render/SDL_yuv_sw_c.h (GitHub permalink)

struct SDL_SW_YUVTexture
{
  ....
  int w, h;
  ....
};
  
typedef struct SDL_SW_YUVTexture SDL_SW_YUVTexture;

That is used in a certain fragment:

File: /SDL/src/render/SDL_yuv_sw.c (GitHub permalink)

int SDL_SW_UpdateYUVTexture(SDL_SW_YUVTexture *swdata, const SDL_Rect *rect,
                            const void *pixels, int pitch
{
  ....
  SDL_memcpy(swdata->pixels, pixels,
       (size_t)(swdata->h * swdata->w) + 
            2 * ((swdata->h + 1) / 2) * ((swdata->w + 1) / 2));
  ....
}

When people say that you should consult the classics from time to time, they are hardly talking about classic type conversion errors in C. Look at the swdata->h * swdata->w expression, then at the types of its operands, and finally at the type to which it's cast. It would be more correct to cast the operands first and then multiply them, don't you think?

The author of the article should note, of course, that he has no idea how big the texture would have to be for its sides to overflow when multiplied. However, to conclude this section, Yours Truly would like to resurrect this article by a former Google employee and author of some Java sections. The article describes how simple mergesort contained a potential overflow bug for years. It had taken a long time, but its hour to "shine" had finally come.

The analyzer issues a warning:

V1028 Possible overflow. Consider casting operands of the 'swdata->h * swdata->w' operator to the 'size_t' type, not the result. SDL/src/render/SDL_yuv_sw.c:125:1

P.S. For example, we can rewrite the code in question this way:

(size_t)(swdata->h) * (size_t)(swdata->w) + 
 (2 * (((size_t)(swdata->h) + 1) / 2)) *
  (((size_t)(swdata->w) + 1) / 2);

However, why pay for security with code readability? Let's rewrite the code as follows:

const size_t h = swdata->h;
const size_t w = swdata->w;
SDL_memcpy(swdata->pixels, pixels,
           h * w + 2 * ((h + 1) / 2) * ((w + 1) / 2));

Shadow of doubt

As Alfred Hitchcock used to say, "A pointer returned from a function is like a woman. The more left to the imagination, the more the excitement". Well, dear reader, does this code excite you?

File: SDL/src/joystick/linux/SDL_sysjoystick.c (GitHub permalink)

static SDL_JoystickGUID LINUX_JoystickGetDeviceGUID(int device_index)
{
  return GetJoystickByDevIndex(device_index)->guid;
}

Saspens starts to build from the very fact of referring to an unchecked pointer, don't you think? Well, maybe GetJoystickByDevIndex can't return anything like that, can it? Let's take a look:

File: SDL/src/joystick/linux/SDL_sysjoystick.c (GitHub permalink)

static SDL_joylist_item *GetJoystickByDevIndex(int device_index)
{
  ....
  if ((device_index < 0) || (device_index >= numjoysticks)) {
    return NULL;   }
  ....
}

NULL, we meet again!

"There is no terror in the bang, only in the anticipation of it!" the maestro once said. However, if the explosion causes the debugger to run, that's a different story!

The analyzer agrees:

V522 There might be dereferencing of a potential null pointer 'GetJoystickByDevIndex(device_index)'. SDL/src/joystick/linux/SDL_sysjoystick.c:1013:1

P.S. The careful reader may note that in the Cargo Cult section above, we specifically recommended using the code that this example requires us to fix. Without a doubt, considering nearly identical code as correct and incorrect can be a bit confusing.

In the example above, we said that it might be safe to write such code because the analyzer will warn you if a function can return NULL after editing the code. We have implied that the static analyzer guarantees security. However, it contradicts the philosophy of defensive programming. So, we'd like to invite our readers to join us in the comments section for a kind of philosophical conversation: how much can and should handy developer tools influence coding style?

Can't see the forest

The following code snippet invites you to look at how you can shoot yourself in the foot for absolutely no good reason:

File: /SDL/src/libm/e_exp.c (GitHub permalink)

double __ieee754_exp(double x)
{
  int32_t k=0;
  .... 
  if(k >= -1021) {
    u_int32_t hy;
    GET_HIGH_WORD(hy,y);
    SET_HIGH_WORD(y,hy+(k<<20));    /* add k to y's exponent */
    return y;
  }
  ....
}

The k variable can take negative values, then it shifts twenty bits to the left, shouting and hooting merrily.

In the days when dinosaurs roamed the earth, and programs were compiled with Borland, developers used different platforms (or so they say!). These platforms didn't necessarily use two's complement to represent negative numbers. The C language was designed with this diversity in mind. Although, with the interests of so many parties to consider, it can be a little difficult to guarantee some things. Such a bitwise shift operation can't provide a reasonable platform-independent result if the left operand is a negative number. That's why it's UB!

In the meantime, are there any of our esteemed readers who still work with "unorthodox" platforms? Please tell us in the comments — it's very interesting!

The analyzer issued a message:

V610 Undefined behavior. Check the shift operator '<<'. The left operand is negative ('k' = [-1021..2147483647]). SDL/src/libm/e_exp.c:159:1

Here is a similar and more interesting fragment where the apparent code complexity played its cruel joke and hid the root of a possible issue:

File: /SDL/src/libm/e_rem_pio2.c (GitHub permalink)

int32_t attribute_hidden __ieee754_rem_pio2(double x, double *y)
{
  int32_t e0,i,j,nx,n,ix,hx;
  GET_HIGH_WORD(hx,x);         /* high word of x */
  ix = hx&0x7fffffff;
  ....
  if(ix<=0x413921fb) {
    ....
    if(hx<0) {y[0] = -y[0]; y[1] = -y[1]; return -n;}
    else     return n;
  }
  ....
  if(ix>=0x7ff00000) {        /* x is inf or NaN */
    y[0]=y[1]=x-x; return 0;
  }
  ....
  e0  = (ix>>20)-1046;        /* e0 = ilogb(z)-23; */

  SET_HIGH_WORD(z, ix - ((int32_t)(e0<<20)));
}

"Something's moving to and fro there!" Yours Truly exclaimed when he first saw the code. And you won't believe it — it's moving towards UB! Take a look at what the analyzer has issued: the e0 variable can very well take a negative value! Look closer and think about it: the minimum possible value for the ix variable that doesn't cause the function to return early is 0x413921fb + 1. Shift it 20 bits to the right and get 1043. Subtract 1046 and have the UB you are looking for when you shift the value to the left. As my mom used to say when I was a kid, "Don't play with hexes, you'll blow everything up!"

Thank you, analyzer:

V610 Undefined behavior. Check the shift operator '<<'. The left operand is negative ('e0' = [-3..1000]). SDL/src/libm/e_rem_pio2.c:151:1

P.S. We also need to clarify: If your machine has an architecture that uses two's complement to represent negative numbers, this doesn't mean that the code will work the way you expect. Shifting negative numbers to the left is always UB. All other ideas about UB and how the code actually works are dangerous, if not downright wrong.

The fastest hand in the West

As my grandma used to say, "Code in haste, and debug at leisure!" "And when coding, you better avoid bugs," Yours Truly adds. Let's look at the following code:

File: /SDL/src/joystick/linux/SDL_sysjoystick.c (GitHub permalink)

static SDL_bool LINUX_JoystickGetGamepadMapping(
    int device_index, SDL_GamepadMapping *out)
{
  ....
  joystick = (SDL_Joystick *)SDL_calloc(sizeof(*joystick), 1);
  joystick->magic = &SDL_joystick_magic;

  if (joystick == NULL) {
    SDL_OutOfMemory();
    return SDL_FALSE;
  }
  ....
}

In this case, we don't really care what exactly is in the joystick pointer. What's important is that the programmer addresses the pointer first, and only then asks if it can actually be done. In fact, there is no better place in this article for a proverb that truly deserves the name of royal — Festina lente!

The analyzer puts it this way:

V595 The 'joystick' pointer was utilized before it was verified against nullptr. Check lines: 2119, 2120. SDL/src/joystick/linux/SDL_sysjoystick.c:2120:1

That's the way it was meant to be!

To conclude our review of today's collection, I would like to remind you that errors can also arise from a simple desire to fix them later:

File: SDL/src/render/software/SDL_triangle.c (GitHub permalink)

int SDL_SW_FillTriangle(SDL_Surface *dst, 
                        SDL_Point *d0, SDL_Point *d1, SDL_Point *d2,
                        SDL_BlendMode blend, 
                        SDL_Color c0, SDL_Color c1, SDL_Color c2)
{
  ....
  if (dst->format->Amask) {
    color = SDL_MapRGBA(tmp->format, c0.r, c0.g, c0.b, c0.a);
  } else {
    // color = SDL_MapRGB(tmp->format, c0.r, c0.g, c0.b);
    color = SDL_MapRGBA(tmp->format, c0.r, c0.g, c0.b, c0.a);
  }
  ....
}

"Maybe that was the intention?" the inquisitive reader may ask. Well, if that was the intention, why is there a condition?

The analyzer poses the same question:

V523 The 'then' statement is equivalent to the 'else' statement. SDL/src/render/software/SDL_triangle.c:341:1

Interview

Do you remember what Yours Truly said about the little interview with the SDL creator at the very beginning of the article? We asked Sam Lantinga, the project father and lead maintainer, a few questions. In turn, he answered them, and we are very grateful for that! I hope you, dear reader, will find this interview interesting as well. Would you like to see more collaborations like this in the future? Please let us know in the comments.

It seems like it takes a fair degree of proficiency to work on a project of SDL level. Could you share how many developers are involved in it?

There are about half a dozen people who regularly work on features and improvements and review submissions from other developers. And of course, we get a huge number of people who swing by and request features or provide PRs to address issues. Our GitHub repository lists over 400 contributors, and we really appreciate all the help!

Some functions in SDL deal with quite a low-level things, like a double exponential in ieee754_exp. Are there any particular difficulties when writing and testing this kind of code?

For these kinds of things, we try to use code that is already publicly available and well tested, as long as the license is compatible with the SDL license. In this case we use the math functions from uClibc. For code that we write ourselves, we have the luxury of years of experience and enthusiastic developers who are willing to comment on and test our changes.

SDL is pretty much everywhere. On my Arch distro it even came preinstalled (to my great pleasure). Could you share the pipeline that you use to deliver code from a developer's code editor to such a variety of platforms and repos?

We have in the past just provided the signed source code on our website, and distribution maintainers pick up SDL releases and bundle them in their OS updates. These days our releases are available on GitHub, and the process for distribution maintainers is largely the same. As part of our release we make binaries available for Windows and Apple platforms, which makes it easier for people who want to drop a new SDL into an existing product to take advantage of bug fixes and improvements.

SDL supports quite a handful of platforms. Which one was the most daunting to implement?

Android and iOS were by far the most challenging because the app model and APIs are so different from desktop platforms.

How do you make sure that you or other contributors does not break other things is SDL while introducing new code or fixing the old one?

We have automated tests that cover a fair amount of SDL functionality, and in general we know from experience what the risk is for touching any particular area of code. If something is risky we'll put it in early in a milestone so it gets lots of testing before going live.

Do you test SDL separately for every platform?

Yes.

Could you share the stack that you use for testing?

We use a variety of test programs included in the SDL repository and run them using GitHub Actions in response to commits to make sure they don't cause issues. One of the really nice things about the GitHub ecosystem is that the SDL tests run on pull requests from other people, so they can verify that their code works well before we merge it into the main repository.

Conclusion

That was a great adventure! The opportunity to read the code that industry leaders have been writing for decades is priceless, as is the opportunity to make a modest contribution to its development.

I hope you, dear reader, had as much fun reading this article as its author had researching the project! As always, Yours Truly is looking forward to reading your thoughts, wishes, and recommendations in the comments section.

And, of course, Crusader Lord Lantinga – we cannot be more grateful!

Thank you for making it to the end! El Psy Kongroo.