Let's make a small library like GLFW to open a window and display a bitmap on it. The library must be easy to use and to integrate into a project, it must also be cross-platform. The complete source code can be found here.
Building our program should not require any build system like CMake, instead we will use a simple script to compile for each operating system.
For windows with GCC:
    
gcc.exe main.c -l gdi32 -mwindows
    build_windows_gcc.bat
    
    Or with MSVC:
    
    vcvarsall.bat
cl main.c user32.lib gdi32.lib /link /SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup
    build_windows_msvc.bat
    
    On Linux with X11:
    
    #!/bin/bash
gcc main.c -l X11 -D X11
    build_linux_x11.sh
    
    Or with Wayland:
    
    #!/bin/bash
gcc main.c -l wayland-client -D WAYLAND
    build_linux_wayland.sh
    
    We want to abstract all the ugly platform specific details and display a blank window simply like this:
    #include "window.h"
int main(){
    int err = create_window("my window", 500, 500);
    if(err){
        return 1;
    }
    while(event_loop()){
    }
    return 0;
}
    main.c
    
    The window.h header file will contain all the functions we need to create and manipulate a window, we will implement each one depending on the target OS and windowing system.
    #pragma once
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
int window_width, window_height;
#ifdef _WIN32
// Windows implementation
#elif __linux__
#ifdef X11
// Linux with X11 implementation
#elif WAYLAND
// Linux with Wayland implementation
#endif
#endif
    window.h
    
    Let's first make a blank window using the Win32 API. To understand what each functions does exactly, search for it in the Windows documentation.
    
/* IMPORTANT NOTE
Keep variable names explicit, "hwnd" doesn't make any sense but
"window_handle" does. The windows API is confusing enough, don't
make it worse with unreadable variable names.
*/
#include <windows.h>
HWND window_handle;
void close_window(){
    DestroyWindow(window_handle);
    UnregisterClass("WindowClass", GetModuleHandle(0));
}
/* This function handles the events (messages) that our window
receives, for now we are only handling closing our window (WM_CLOSE) */
LRESULT CALLBACK window_procedure(HWND window_handle, UINT message, WPARAM w_param, LPARAM l_param){
    switch(message){
        case WM_CLOSE:
            close_window();
            break;
        case WM_DESTROY:
            PostQuitMessage(0);
            break;
        default:
            return DefWindowProc(window_handle, message, w_param, l_param);
    }
    return 0;
}
/* This function formats the error code returned by GetLastError()
into a string, and prints it along with the error message we passed to it */
void print_last_error(char* msg){
    wchar_t buff[256];
    FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM |
        FORMAT_MESSAGE_IGNORE_INSERTS |
        FORMAT_MESSAGE_MAX_WIDTH_MASK,
        NULL, GetLastError(),
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
        buff, (sizeof(buff)/sizeof(wchar_t)), NULL);
    fprintf(stderr, "%s, %S\n", msg, buff);
}
int create_window(char* title, int width, int height){
    window_width = width;
    window_height = height;
    /* A window class is a structure that defines attributes
    that we can use for multiple windows (for example the icon).
    We will only set the minimum required attributes for now */
    const char* window_class_name = "WindowClass";
    WNDCLASS window_class = {0};
    window_class.lpfnWndProc = window_procedure;
    window_class.hInstance = GetModuleHandle(0);
    window_class.lpszClassName = window_class_name;
    if(!RegisterClass(&window_class)){
        print_last_error("RegisterClass: Error when creating window class");
        return 1;
    }
    DWORD window_style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX;
    /* We want the inside of the window (client area) to be width*height.
    To do that we have to calculate the size of the whole window which is
    size of the client area + size of the window decoration (title bar, borders)
    */
    RECT window_rect = {0, 0, window_width, window_height};
    AdjustWindowRect(&window_rect, window_style, FALSE);
    window_handle = CreateWindowEx(0,
        window_class_name,
        title,
        window_style,
        CW_USEDEFAULT, CW_USEDEFAULT,
        window_rect.right-window_rect.left,
        window_rect.bottom-window_rect.top,
        NULL, NULL, GetModuleHandle(0), NULL);
    if(!window_handle){
        print_last_error("CreateWindowEx: Error when creating window");
        return 1;
    }
    ShowWindow(window_handle, 5);
    return 0;
}
int event_loop(){
    MSG message;
    int ret = GetMessage(&message, NULL, 0, 0);
    TranslateMessage(&message);
    DispatchMessage(&message);
    return ret;
}
    window.h
    
    Here is the full code. We get this result:
    To call the X11 documentation bad would be an understatement, the best resource I could find is The Xlib Manual.
#include <X11/Xlib.h>
#include <X11/Xutil.h>
Display* display;
Window window;
Atom delete_window_atom;
int create_window(char* title, int width, int height){
    window_width = width;
    window_height = height;
    display = XOpenDisplay(0);
    if(display == NULL){
        fprintf(stderr, "XOpenDisplay: Error opening display\n");
        return 1;
    }
    /* We will need the visual information and the colormap later when
       displaying a bitmap */
    XVisualInfo visual_info;
    XMatchVisualInfo(display, DefaultScreen(display), 32, TrueColor, &visual_info);
    XSetWindowAttributes window_attributes;
    window_attributes.colormap = XCreateColormap(display, DefaultRootWindow(display), visual_info.visual, AllocNone);
    window_attributes.border_pixel = 0;
    window_attributes.background_pixel = 0;
    window = XCreateWindow(display, DefaultRootWindow(display),
        0, 0, width, height, 0,
        visual_info.depth,
        InputOutput,
        visual_info.visual,
        CWColormap | CWBorderPixel | CWBackPixel,
        &window_attributes);
    XSetStandardProperties(display, window, title, title, None, NULL, 0, NULL);
    /* We tell the window manager how large we want our window to be.
       We can't enforce this, hence why it's called a hint. */
    XSizeHints size_hints;
    size_hints.flags = PMinSize | PMaxSize;
    size_hints.min_width = window_width;
    size_hints.min_height = window_height;
    size_hints.max_width = window_width;
    size_hints.max_height = window_height;
    XSetWMNormalHints(display, window, &size_hints);
    XSelectInput(display, window, ExposureMask | KeyPressMask | KeyReleaseMask);
    /* An event returned from the window manager when clicking the close
       button */
    delete_window_atom = XInternAtom(display, "WM_DELETE_WINDOW", 0);
    XSetWMProtocols(display, window, &delete_window_atom, 1);
    XMapWindow(display, window);
    return 0;
}
int event_loop(){
    XEvent event;
    XNextEvent(display, &event);
    if(event.type == ClientMessage){
        if(event.xclient.data.l[0] == delete_window_atom){
            return 0;
        }
    }
    return 1;
}
void close_window(){
    XDestroyWindow(display, window);
    XCloseDisplay(display);
}
    Here is the full code. We get this result:
    
    #include "window.h"
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
int main(){
    int width, height, channels;
    unsigned char* data = stbi_load("peppers.png", &width, &height, &channels, 3);
    if(!data){
        return 0;
    }
    init_buffer(width, height);
    for(int i = 0; i < width*height; i++){
        /* We store each pixel color in a 32 bits integer.
           The first 8 bits are alpha, followed by RGB in 24 bits.
           For example the color rgba(255, 0, 0, 255) becomes 0xFFFF0000
        */
        buffer[i] = (0xff<<24)+(data[i*3]<<16)+(data[i*3+1]<<8)+data[i*3+2];
    }
    int err = create_window("my window", 512, 512);
    if(err){
        return 1;
    }
    while(event_loop()){
    }
    return 0;
}
    main.c
    
    We define the init_buffer function in window.h:
    
    #pragma once
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
int window_width, window_height;
uint32_t* buffer;
void init_buffer(int width, int height){
    buffer = malloc(width*height*sizeof(uint32_t));
}
...
    window.h
    
    
    HWND window_handle;
BITMAPINFO bitmap_info = {0};
void draw_buffer(HDC device_context){
    StretchDIBits(device_context, 0, 0, window_width, window_height,
                    0, 0, window_width, window_height, buffer, &bitmap_info,
                    DIB_RGB_COLORS, SRCCOPY);
}
LRESULT CALLBACK window_procedure(HWND window_handle, UINT message, WPARAM w_param, LPARAM l_param){
    switch(message){
        case WM_PAINT: {
            PAINTSTRUCT paint;
            HDC device_context = BeginPaint(window_handle, &paint);
            draw_buffer(device_context);
            EndPaint(window_handle, &paint);
            break;
        }
        case WM_CLOSE:
            close_window();
            break;
        ...
    window.h
    
    We also specify the dimension and depth of our DIB (A "Device-Independent Bitmap", which for our purposes is just a bitmap):
    
    int create_window(char* title, int width, int height){
    ...
    bitmap_info.bmiHeader.biSize = sizeof(bitmap_info.bmiHeader);
    bitmap_info.bmiHeader.biWidth = window_width;
    /* We specify a negative height so our DIB is displayed
       top to bottom */
    bitmap_info.bmiHeader.biHeight = -window_height;
    /* This is always 1 */
    bitmap_info.bmiHeader.biPlanes = 1;
    /* 32 bits for each pixel */
    bitmap_info.bmiHeader.biBitCount = 32;
    /* We don't use any image compression */
    bitmap_info.bmiHeader.biCompression = BI_RGB;
    ShowWindow(window_handle, 5);
    return 0;
}
    window.h
    
    Here is the full code. We get this result:
    Here is the full code. We get this result: