2
\$\begingroup\$

I am making a C library that abstracts window creation with support for the new Vulkan API under a unified API; I have a github repository that you can check out.

main.c

#include "vkwf.h"

int main()
{
    VKWFWindow* window = VKWFCreateWindow("Test Window", 800, 600);

    while (!VKWFWindowShouldClose(window))
    {
        VKWFWindowUpdate(window);
    }

    free(window);

    return 0;
}

The way I am handling this is creating a general vkwf.h file, that includes a list of functions, like this:

vkwf.h

#pragma once

#ifdef __cplusplus
extern "C" {
#endif

#ifdef VKWF_PLATFORM_WINDOWS
#include "win32_window.h"
#elif VKWF_PLATFORM_MACOS
#include "macos_window.h"
#elif VKWF_PLATFORM_LINUX
#include "linux_window.h"
#endif

VKWFWindow* VKWFCreateWindow(const char* title, int width, int height)
{
    return VKWFPlatformCreateWindow(title, width, height);
}

VKWFBool VKWFWindowShouldClose(VKWFWindow* window)
{
    return VKWFPlatformWindowShouldClose(window);
}

void VKWFWindowUpdate(VKWFWindow* window)
{
    VKWFPlatformUpdate(window);
}

void VKWFDestroyWindow(VKWFWindow* window)
{
    VKWFPlatformDestroyWindow(window);
}

#ifdef __cplusplus
}
#endif

All VKWFPlatformX() functions are functions that get defined by the platform_window.h files, like this:

win32_window.h (short)

#define VKWFPlatformCreateWindow(title,width,height) VKWFWin32CreateWindow(title,width,height)
#define VKWFPlatformWindowShouldClose(window) VKWFWin32WindowShouldClose(window)
#define VKWFPlatformUpdate(window) VKWFWin32Update(window)
#define VKWFPlatformDestroyWindow(window) VKWFWin32DestroyWindow(window)

Does this look like a good approach? Are there any drawbacks, or things that i could improve?

\$\endgroup\$
3
\$\begingroup\$

I wouldn't use #define macros. They expose the internal naming convention of your OS dependent functions to the world. Once you send out your library to others [as a shared library], you can never change your internal OS dependent names. The important thing is that public facing names be functions.

For example, if two systems used gcc and created ELF binaries that only called your library functions, could you compile on (e.g.) FreeBSD and that binary would run without rebuild on linux. There are other issues with doing this, so it [probably] isn't practical, but, it's something to think about.

A better way [what I've done when faced with a similar problem] is to put the OS dependent code in a .c and add static to the definitions.

The public functions just call the static ones. The static function names are the same, regardless of platform

The optimizer will either inline the static OS dependent function or will use tail call optimization. So, it's just as fast as macros but a lot cleaner.

Side note: Your naming convention is a bit [MS] Windows centric (i.e. camel hump case). I prefer snake case (e.g. see GTK, etc.). For an example, see the bottom.


vkwh.h:

#pragma once

#ifdef __cplusplus
extern "C" {
#endif

VKWFWindow* VKWFCreateWindow(const char* title, int width, int height);
VKWFBool VKWFWindowShouldClose(VKWFWindow* window);
void VKWFWindowUpdate(VKWFWindow* window);
void VKWFDestroyWindow(VKWFWindow* window);

#ifdef __cplusplus
}
#endif

vkwf.c:

#include "vkwf.h"

#ifdef VKWF_PLATFORM_WINDOWS
#include "win32_window.c"
#elif VKWF_PLATFORM_MACOS
#include "macos_window.c"
#elif VKWF_PLATFORM_LINUX
#include "linux_window.c"
#endif

VKWFWindow* VKWFCreateWindow(const char* title, int width, int height)
{
    return VKWFPlatformCreateWindow(title, width, height);
}

VKWFBool VKWFWindowShouldClose(VKWFWindow* window)
{
    return VKWFPlatformWindowShouldClose(window);
}

void VKWFWindowUpdate(VKWFWindow* window)
{
    VKWFPlatformUpdate(window);
}

void VKWFDestroyWindow(VKWFWindow* window)
{
    VKWFPlatformDestroyWindow(window);
}

win32_window.c:

#include "vkwf.h"

static VKWFWindow*
VKWFPlatformCreateWindow(const char* title, int width, int height)
{
    // ...
}

static VKWFBool
VKWFPlatformWindowShouldClose(VKWFWindow* window)
{
    // ...
}

static void
VKWFPlatformWindowUpdate(VKWFWindow* window)
{
    // ...
}

static void
VKWFPlatformDestroyWindow(VKWFWindow* window)
{
    // ...
}

Here's an example of the snake case for the public functions. Note that since the platform specific functions are static, then can use shorter prefixes:

#include "vkwf.h"

#ifdef VKWF_PLATFORM_WINDOWS
#include "win32_window.c"
#elif VKWF_PLATFORM_MACOS
#include "macos_window.c"
#elif VKWF_PLATFORM_LINUX
#include "linux_window.c"
#endif

VKWFWindow* VKWF_create_window(const char* title, int width, int height)
{
    return platform_create_window(title, width, height);
}

VKWFBool VKWF_window_should_close(VKWFWindow* window)
{
    return platform_window_should_close(window);
}

void VKWF_window_update(VKWFWindow* window)
{
    platform_update(window);
}

void VKWF_destroy_window(VKWFWindow* window)
{
    platform_destroy_window(window);
}
\$\endgroup\$
1
\$\begingroup\$

Use libraries

I would suggest a completely different approach. Keeping the common vkwf.h header makes sense because it presents a uniform interface to programmers using it. However, the actual library used is going to be dependent on the platform. That is, there is likely little use in trying to compile the Windows version of the implementation for use on Linux. Instead, what's more likely is that you will be compiling the Windows version using a compiler that emits Windows code, and a Linux version using a compiler that emits Linux code, etc. (Some variations can occur, such as compiling under Cygwin.)

So the way to do this is instead to provide separate implementations that each compile into the platform-appropriate static or shared library (e.g. DLLs for Windows and .so files for Linux).

Don't abuse headers

Putting code into a .h file is not a good idea. The header should contain the interface only and not produce any object file output. All executable code and memory allocations (e.g. static structures and variables) should be in .c files. There are many reasons for this advice, with the most important one being that if you have multiple files in a project that each need to #include your header, it will fail if you fail to adhere to this guideline. Further, it makes the library brittle and unpredictable, because it can introduce strange header-ordering dependencies that are hard to debug. Don't do this!

Study a successful library for ideas

There are many examples of successful cross-platform libraries. I'd recommend looking at how the very successful zlib library was written and emulating that.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.