Opening windows in linux with sockets, bare hands and 200 lines of C



Simple x11 window opened with
sockets

Intro

In this post I want to create a single file C file program to open a windows inside Linux without using xlib or any similar libraries. The idea is to explore X11 protocol and see how it is used to interact with X server to create windows.

Before I had strong association that X11 was some magic thing to manipulate windows and it’s resources. I was very surprised to learn that it is actually just a “regular” network protocol for two parties to communicate like HTTP, FTP, IMAP, SMPT and etc. But if in IMAP your have a server that contains all your emails and you send it commands to get information about emails and get their content. In X11 you have a server that contains all your windows and its resources and you just communicate with it via a connection.

To do this the only resource we need is X11 documentation. It is a very small document and can be easily consumed to better understand window communication in linux systems that still use Xorg for window management. The strange thing is that this document is way more approachable that Xlib’s documentation which totally broke my illusion that Xlib was supposed to simplify X11.

In this post we will try to implement this model: X11 Initialization
request

Open connection

Xorg uses unix sockets instead of regular sockets for the communication with the apps. (You can switch it to using regular sockets if you want to monitor connections in wireshark. Look in one of my old articles on how to do it) For us, regular users, it does not really matter as everything will be pretty much the same with only tiny difference in socket setup.

int Socket = socket(AF_UNIX, SOCK_STREAM, 0);
VerifyOrDie(Socket > 0, "Couldn't open a socket(...)");

struct sockaddr_un Address;
memset(&Address, 0, sizeof(struct sockaddr_un));
Address.sun_family = AF_UNIX;
strncpy(Address.sun_path, "/tmp/.X11-unix/X0", sizeof(Address.sun_path)-1);

int Status = connect(Socket, (struct sockaddr *)&Address, sizeof(Address));
VerifyOrDieWidthErrno(Status == 0, "Couldn't connect to a unix socket with connect(...)");

We just get socket from the kernel with socket(…) but use AF_UNIX instead of AF_INET to tell it that we are plan to setup unix socket communication. Then we use regular connect(…) to connect the socket. Another difference is that we are using struct sockaddr_un for the connection description and set it’s path to: /tmp/.X11-unix/X0

To simplify error checking and to reduce code size I am using this two utility function for easier error checking and reporting. They check if condition is correct. If not they just print error and fully exit out of program execution.

void VerifyOrDie(int IsSuccess, const char *Message) {
    if(!IsSuccess)  {
        fprintf(stderr, "%s", Message);
        exit(13);
    }
}

void VerifyOrDieWidthErrno(int IsSuccess, const char *Message) {
    if(!IsSuccess)  {
        perror(Message);
        exit(13);
    }
}

This is pretty much. With just this we are ready to comuunicate with the X server. Initially server expects a connection iniation from user and the very first byte of the request must be a identification of communication byte order: little endian or big endian. Documentation calls it MSB (Most significant byte) and LSB(Least signification byte). After this byte is sent all data will be processed as either little endian or big endian untill connection termination. Two possible values for this first byte is ’l’ (0x6c) or ‘B’ (0x42).

X11 Initialization request

In earlier implementation of this program I used struct to hold this initialization data and later just serialize it to send over the socket. But then I came to a conclusion that for demonstration purposes using just an array and filling it ‘by hand’ is a better and simpler approach to show the concept.

Initial request seems like a lot of information to fill but if we skip authorization process we can drastically reduce amount of code we need to write and understand. So for this I chose to not use authorization with a little trick. This give us just two parameters that we need to fill: stream endiannes and major version (minor version is set automatically).

uint8_t InitializationRequest[12] = {};
InitializationRequest[0] = 'l';
InitializationRequest[2] = 11;

We are creating an array of 12 bytes. First byte we set to ’l’ or 0x6c and third byte to 11 to indicate 11th version of the protocol. Everything else is set to 0 by initialization and indicates to X server that we will not be using and sending any authorization data.

For this simplification to work we need to disable regular cookie based authentication and allow all app on local machine to connect to X server directly. To to that you need to open a terminal and type xhost +local:

Later if you want to revert back you could use xhost -local: to force cookie based authentication back. We could have implemented basic authentication but it is not described in the general X11 documentation page linked above and we decided to stick only to that one document. Plus it is not really that interesting and we can save some screen space. If you forget to disable authentication you will get this error when you run final program:

State: 0
MajorVersion: 11
MinorVersion: 0
AdditionalDataLength: 16
Reason: Authorization required, but no authorization protocol specified

With this details done we can just simply send our initialization request and read back data.

char SendBuffer[16*1024] = {};
char ReadBuffer[16*1024] = {};

int BytesWritten = write(Socket, (char*)&InitializationRequest, sizeof(InitializationRequest));
VerifyOrDie(BytesWritten == sizeof(InitializationRequest), "Wrong amount of bytes written during initialization");

int BytesRead = read(Socket, ReadBuffer, 8);

If we look into documentation, we will see that there are three possible responses to initialization request: error, authentication and sucess. We can safely ignore authentication as we are using it in this example. Error usually means that the connection is refused (with some explanation why). No matter what response type we get back first byte will always indicate response type: 0 - Failed, 2 - Authenticate, 1 - Success. Success response will pretty big and error/authenticate responses will be at least 8 bytes. For this reason we can safely request to read 8 bytes to get basic info about what happend with our request and info about server.

One importannt sidenote is that we create two buffers of 16 killobytes on the stack and use it for reading and writing data. This is safe amount for basic communication with server and helps use to not think about about rosource management in this context. Also we don’t need to clear whole buffer after we done processing it since specification allows having ‘dirty bytes’ in areas that are not in the perimeter of current request/response.

After that we can choose how to continue our processing based on the first byte.

if(ReadBuffer[0] == RESPONSE_STATE_FAILED) {
    DumpResponseError(Socket, ReadBuffer);
}
else if(ReadBuffer[0] == RESPONSE_STATE_AUTHENTICATE) {
    AuthenticateX11();
}
else if(ReadBuffer[0] == RESPONSE_STATE_SUCCESS) {
...
}

Here are utility functions to dump information on error.

void DumpResponseError(int Socket, char* ReadBuffer) {
        uint8_t ReasonLength = ReadBuffer[1];
        uint16_t MajorVersion = *((uint16_t*)&ReadBuffer[2]);
        uint16_t MinorVersion = *((uint16_t*)&ReadBuffer[4]);
        uint16_t AdditionalDataLength = *((uint16_t*)&ReadBuffer[6]); // Length in 4-byte units of "additional data"
        uint8_t *Message = (uint8_t*)&ReadBuffer[8];

        int BytesRead = read(Socket, ReadBuffer + 8, READ_BUFFER_SIZE-8);

        printf("State: %d\n", ReadBuffer[0]);
        printf("MajorVersion: %d\n", MajorVersion);
        printf("MinorVersion: %d\n", MinorVersion);
        printf("AdditionalDataLength: %d\n", AdditionalDataLength);
        printf("Reason: %s\n", Message);
}


void AuthenticateX11() {
    fprintf(stderr, "Current version of the app does not support authentication.\n");
    exit(13);
}

X11 Initialization request

So if we get anything but success we just print debug information and exit the program. When we get normal success response we have to do some work to process returned response.

BytesRead = read(Socket, ReadBuffer + 8, READ_BUFFER_SIZE-8);

uint16_t MajorVersion = *((uint16_t*)&ReadBuffer[2]);
uint16_t MinorVersion = *((uint16_t*)&ReadBuffer[4]);
uint16_t AdditionalDataLength = *((uint16_t*)&ReadBuffer[6]); // Length in 4-byte units of "additional data"

uint32_t ResourceIdBase = *((uint32_t*)&ReadBuffer[12]);
uint32_t ResourceIdMask = *((uint32_t*)&ReadBuffer[16]);
uint16_t LengthOfVendor = *((uint16_t*)&ReadBuffer[24]);
uint8_t NumberOfFormants = *((uint16_t*)&ReadBuffer[29]);
uint8_t *Vendor = (uint8_t *)&ReadBuffer[40];

int32_t VendorPad = PAD(LengthOfVendor);
int32_t FormatByteLength = 8 * NumberOfFormants;
int32_t ScreensStartOffset = 40 + LengthOfVendor + VendorPad + FormatByteLength;

uint32_t RootWindow = *((uint32_t*)&ReadBuffer[ScreensStartOffset]);
uint32_t RootVisualId = *((uint32_t*)&ReadBuffer[ScreensStartOffset + 32]);

GlobalIdBase = ResourceIdBase;
GlobalIdMask = ResourceIdMask;
GlobalRootWindow = RootWindow;
GlobalRootVisualId = RootVisualId;

First of all we read all output from server. We ask to read buffer size (16k) minus 8 bytes that we already read an put it inside our buffer by offsetting 8 bytes of already read data. In my system this second read(…) returned 9804 bytes or 9804+8 = 9812 total response bytes for our initialization request. Documentation show that this binary format contains quite a few information: basic server info, root window, data formats, types, screen info, depth info, visual types and etc. For a full blown production system it better to parse it it all but in our exploratory phase we can get just the basics and get away with it.

Major version, minor version, additional length are not required but I got them to verify that everything is working as expected.

Then we get Resource Id Base and Resource Id Mask. What are these? Well unfortunately even though X server is managing window resources it delegates “naming” resource to us (the client). It might me for optimisation purposes. So what this means is that when we need to create a window, crete graphic context or font we must provide the “name” for it. This “name” is just an increasing integer value with some processing. Id does not have to be contigous and can be reused once resource is freed but we won’t be doing ID management here just get increasing numbers without reusing. Also Resource ID’s have never top three bits set. To get id we take our local id and OR it with resource base id.

In my system when I run this code I get base id of ‘0x3200000’ and mask of ‘0x1fffff’. So let’s if I have a local resouce if of 1 then I OR it with 0x3200000 and just mask it with 0x1fffff just to make sure that id has to three bits ’turned off’.

Next we use NumberOfFormants, VendorPad, FormatByteLength to get ScreensStartOffset which in turn is used to get offset to first screen data bytes. From this we need RootWindow and RootVisualId. And this is pretty much all that wee needed from all that 9kb response. Then I just put them into global variables for later use. (A better approach is to contain it all in a struct but we are not architecturing software here but just exploring a protocol). PAD macro just calculates padding to make sure that data length is multiple of 4;

And with this we conclude intiation. There are a lot of words but basically we just send one small request and get back large response.

Creating window

Here we will be create a request to create a window.

X11 Initialization
request

int32_t WindowId = GetNextId();
int32_t Depth = 0;
int32_t X = 100;
int32_t Y = 100;
uint32_t Width = 600;
uint32_t Height = 300;
uint32_t BorderWidth = 1;
int32_t CreateWindowFlagCount = 2;
int RequestLength = 8+CreateWindowFlagCount;

SendBuffer[0] = X11_REQUEST_CREATE_WINDOW;
SendBuffer[1] = Depth;
*((int16_t *)&SendBuffer[2]) = RequestLength;
*((int32_t *)&SendBuffer[4]) = WindowId;
*((int32_t *)&SendBuffer[8]) = GlobalRootWindow;
*((int16_t *)&SendBuffer[12]) = X;
*((int16_t *)&SendBuffer[14]) = Y;
*((int16_t *)&SendBuffer[16]) = Width;
*((int16_t *)&SendBuffer[18]) = Height;
*((int16_t *)&SendBuffer[20]) = BorderWidth;
*((int16_t *)&SendBuffer[22]) = WINDOWCLASS_INPUTOUTPUT;
*((int32_t *)&SendBuffer[24]) = GlobalRootVisualId;
*((int32_t *)&SendBuffer[28]) = X11_FLAG_WIN_EVENT | X11_FLAG_BACKGROUND_PIXEL;
*((int32_t *)&SendBuffer[32]) = 0xff000000;
*((int32_t *)&SendBuffer[36]) = X11_EVENT_FLAG_EXPOSURE | X11_EVENT_FLAG_KEY_PRESS;

BytesWritten = write(Socket, (char *)&SendBuffer, RequestLength*4);

Here we just setup basic variables for readability puproses and the fill an array SenbBuffer array with relevant data. And then just send it. First byte is always describes request. In this case we are creating a window and set it to 1. Depth parameter is not that important and we can set it to 0. Next we calclate RequestLength. This always indicates total request size including header and extra parameters. The only caveat is that it is measured in 4 byte chunks. So we have 32 required bytes and some extra. Thus we have 32/4=8 bytes and extra 2 four byte blocks for extra data.

Documentation explains this dynamic LISTofVALUE as “The value-list contains one value for each bit set to 1 in the mask, from least significant to most significant bit in the mask.” Since the mask is X11_FLAG_WIN_EVENT | X11_FLAG_BACKGROUND_PIXEL or 0x00000002 | 0x00000800. This in turn give us ‘0b100000000010’. So background pixel will be first value to be provided and second is a list of event masks. For the background we provided just a black color with 0xff000000. You can easily change it to green by replacing it with 0xff00ff00.

Next value (starting byte 36) we provide xored list of events that we want to recieve back from X server. In this case we want to get back Exposure (0x8000) and KeyPress (0x0001) events. These will be important later once we start processing event from server.

One thing I didn’t show is GetNextId(). It is just a utility function of the functionality discussed earlier about how to “generate” new if for a resource. For simplicity it uses global index of last id and increases it by on each iteration.

int32_t GetNextId() {
    int32_t Result = (GlobalIdMask & GlobalId) | GlobalIdBase;
    GlobalId += 1;
    return Result;
}

Mapping (showing) window

By this time we have already create a window resource on the server side. It is not shown to the screen yet because we need to map it first. Compared to creating window this request is pretty small.

SendBuffer[0] = X11_REQUEST_MAP_WINDOW;
SendBuffer[1] = 0; // NOTE: Unused
*((int16_t *)&SendBuffer[2]) = 2;
*((int32_t *)&SendBuffer[4]) = WindowId;

BytesWritten = write(Socket, (char *)&SendBuffer, 2*4);

X11 Initialization
request

As usual we user first byte to set request type. This time it is “Map Window” or 0x8. Second byte is unused and can be set to anything. Third byte is request size which we set to 2 (result of 8/4). And the last byte parameter we set the window ID created in the last step.

With this we are pretty much done with basic window opening. Now if we just put something like sleep(5) at the end of the code we will get a window which will be shown for 5 seconds. Then after the 5 seconds are passed the program will close and X server will recycle all resources.

Event loop

Instead of sleeping let’s try to actually create a loop that will listen events sent back to us from server and just block when there is nothing to do or to show. Since when creating we indicated that we are interested in Exposure and KeyPress events there will be event to notify about regions that need to up repainted and pressed keys.

For this I chose to use linux regular polling mechanism which will block us untill we have something to do. Nothing special. Just setup one socket descriptor into list.

struct pollfd PollDescriptors[1] = {};
PollDescriptors[0].fd = Socket;
PollDescriptors[0].events = POLLIN;
int32_t DescriptorCount = 1;

Then we can create an endless loop which will be check if a IsProgramRunning variable set to true or false (1 or 0).

int32_t IsProgramRunning = 1;
while(IsProgramRunning){
    int32_t EventCount = poll(PollDescriptors, DescriptorCount, -1);

    if(PollDescriptors[0].revents & POLLERR) {
        fprintf(stderr, "------- Error\n");
    }

    if(PollDescriptors[0].revents & POLLHUP) {
        printf("---- Connection close\n");
        IsProgramRunning = 0;
    }

    GetAndProcessReply(PollDescriptors[0].fd);
}

When we call poll(…) we set timeout to -1 to make sure it never times out. Once there is an event the program will unblock and continue execution. Then we check if the event was error or we the connection hung up. On error we just log it and continue like nothing happened. On POLLHUP (program close) just terminate program. Else we just process the reply.

void PrintResponseError(char *Data, int32_t Size) {
    char ErrorCode = Data[1];
    printf("\033[0;31m");
    printf("Response Error: [%d]", ErrorCode);
    printf("\033[0m\n");
}

void PrintAndProcessEvent(char *Data, int32_t Size) {
    char EventCode = Data[0];
    printf("Some event occured: %d\n", EventCode);
}

void GetAndProcessReply(int Socket) {
    char Buffer[1024] = {};
    int32_t BytesRead = read(Socket, Buffer, 1024);
    uint8_t Code = Buffer[0];

    if(Code == 0) {
        PrintResponseError(Buffer, BytesRead);
    } else if (Code == 1) {
        printf("---------------- Reply to request\n");
    } else {
        PrintAndProcessEvent(Buffer, BytesRead);
    }
}

And with this we have a “fully functional” window with event loop. Even though the processing is pretty simple and we just log errors and events but still from here it will be that hard to extend and try more advanced events processing.

Whole code for simple version for this program (about 200 lines of code)

#include <sys/socket.h>
#include <sys/un.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>


int32_t GlobalId = 0;
int32_t GlobalIdBase = 0;
int32_t GlobalIdMask = 0;
int32_t GlobalRootWindow = 0;
int32_t GlobalRootVisualId = 0;

#define READ_BUFFER_SIZE 16*1024

#define RESPONSE_STATE_FAILED 0
#define RESPONSE_STATE_SUCCESS 1
#define RESPONSE_STATE_AUTHENTICATE 2

#define X11_REQUEST_CREATE_WINDOW 1
#define X11_REQUEST_MAP_WINDOW 8
#define X11_REQUEST_IMAGE_TEXT_8 76
#define X11_REQUEST_OPEN_FONT 45
#define X11_REQUEST_CREATE_GC 55


#define X11_EVENT_FLAG_KEY_PRESS 0x00000001
#define X11_EVENT_FLAG_KEY_RELEASE 0x00000002
#define X11_EVENT_FLAG_EXPOSURE 0x8000

#define PAD(N) ((4 - (N % 4)) % 4)

void VerifyOrDie(int IsSuccess, const char *Message) {
    if(!IsSuccess)  {
        fprintf(stderr, "%s", Message);
        exit(13);
    }
}

void VerifyOrDieWidthErrno(int IsSuccess, const char *Message) {
    if(!IsSuccess)  {
        perror(Message);
        exit(13);
    }
}

void DumpResponseError(int Socket, char* ReadBuffer) {
        uint8_t ReasonLength = ReadBuffer[1];
        uint16_t MajorVersion = *((uint16_t*)&ReadBuffer[2]);
        uint16_t MinorVersion = *((uint16_t*)&ReadBuffer[4]);
        uint16_t AdditionalDataLength = *((uint16_t*)&ReadBuffer[6]); // Length in 4-byte units of "additional data"
        uint8_t *Message = (uint8_t*)&ReadBuffer[8];

        int BytesRead = read(Socket, ReadBuffer + 8, READ_BUFFER_SIZE-8);

        printf("State: %d\n", ReadBuffer[0]);
        printf("MajorVersion: %d\n", MajorVersion);
        printf("MinorVersion: %d\n", MinorVersion);
        printf("AdditionalDataLength: %d\n", AdditionalDataLength);
        printf("Reason: %s\n", Message);
}

void AuthenticateX11() {
    fprintf(stderr, "Current version of the app does not support authentication.\n");
    fprintf(stderr, "Please run 'xhost +local:' in your terminal to disable cookie based authentication\n");
    fprintf(stderr, "and allow local apps to communication with Xorg without it.");
}

int32_t GetNextId() {
    int32_t Result = (GlobalIdMask & GlobalId) | GlobalIdBase;
    GlobalId += 1;
    return Result;
}

void PrintResponseError(char *Data, int32_t Size) {
    char ErrorCode = Data[1];
    printf("\033[0;31m");
    printf("Response Error: [%d]", ErrorCode);
    printf("\033[0m\n");
}

void PrintAndProcessEvent(char *Data, int32_t Size) {
    char EventCode = Data[0];
    printf("Some event occured: %d\n", EventCode);

}

void GetAndProcessReply(int Socket) {
    char Buffer[1024] = {};
    int32_t BytesRead = read(Socket, Buffer, 1024);

    uint8_t Code = Buffer[0];

    if(Code == 0) {
        PrintResponseError(Buffer, BytesRead);
    } else if (Code == 1) {
        printf("---------------- Unexpected reply\n");
    } else {
        // NOTE: Event?
        PrintAndProcessEvent(Buffer, BytesRead);
    }
}

int main(){
    int Socket = socket(AF_UNIX, SOCK_STREAM, 0);
    VerifyOrDie(Socket > 0, "Couldn't open a socket(...)");

    struct sockaddr_un Address;
    memset(&Address, 0, sizeof(struct sockaddr_un));
    Address.sun_family = AF_UNIX;
    strncpy(Address.sun_path, "/tmp/.X11-unix/X0", sizeof(Address.sun_path)-1);

    int Status = connect(Socket, (struct sockaddr *)&Address, sizeof(Address));
    VerifyOrDieWidthErrno(Status == 0, "Couldn't connect to a unix socket with connect(...)");

    char SendBuffer[16*1024] = {};
    char ReadBuffer[16*1024] = {};

    uint8_t InitializationRequest[12] = {};
    InitializationRequest[0] = 'l';
    InitializationRequest[1] = 0;
    InitializationRequest[2] = 11;

    int BytesWritten = write(Socket, (char*)&InitializationRequest, sizeof(InitializationRequest));
    VerifyOrDie(BytesWritten == sizeof(InitializationRequest), "Wrong amount of bytes written during initialization");

    int BytesRead = read(Socket, ReadBuffer, 8);

    if(ReadBuffer[0] == RESPONSE_STATE_FAILED) {
        DumpResponseError(Socket, ReadBuffer);
    }
    else if(ReadBuffer[0] == RESPONSE_STATE_AUTHENTICATE) {
        AuthenticateX11();
    }
    else if(ReadBuffer[0] == RESPONSE_STATE_SUCCESS) {
        printf("INIT Response SUCCESS. BytesRead: %d\n", BytesRead);

        BytesRead = read(Socket, ReadBuffer + 8, READ_BUFFER_SIZE-8);
        printf("---------------------------%d\n", BytesRead);

        /* -------------------------------------------------------------------------------- */
        uint8_t _Unused = ReadBuffer[1];
        uint16_t MajorVersion = *((uint16_t*)&ReadBuffer[2]);
        uint16_t MinorVersion = *((uint16_t*)&ReadBuffer[4]);
        uint16_t AdditionalDataLength = *((uint16_t*)&ReadBuffer[6]); // Length in 4-byte units of "additional data"

        uint32_t ResourceIdBase = *((uint32_t*)&ReadBuffer[12]);
        uint32_t ResourceIdMask = *((uint32_t*)&ReadBuffer[16]);
        uint16_t LengthOfVendor = *((uint16_t*)&ReadBuffer[24]);
        uint8_t NumberOfFormants = *((uint16_t*)&ReadBuffer[29]);
        uint8_t *Vendor = (uint8_t *)&ReadBuffer[40];

        int32_t VendorPad = PAD(LengthOfVendor);
        int32_t FormatByteLength = 8 * NumberOfFormants;
        int32_t ScreensStartOffset = 40 + LengthOfVendor + VendorPad + FormatByteLength;

        uint32_t RootWindow = *((uint32_t*)&ReadBuffer[ScreensStartOffset]);
        uint32_t RootVisualId = *((uint32_t*)&ReadBuffer[ScreensStartOffset + 32]);

        GlobalIdBase = ResourceIdBase;
        GlobalIdMask = ResourceIdMask;
        GlobalRootWindow = RootWindow;
        GlobalRootVisualId = RootVisualId;

        printf("Base: %d\n", ResourceIdBase);
        printf("IdMask: %d\n", ResourceIdMask);
        printf("LengthOfVendor: %d\n", LengthOfVendor);

        /* -------------------------------------------------------------------------------- */
        // ------------------------------ Create Window
        int32_t WindowId = GetNextId();
        int32_t Depth = 0;
        int32_t X = 100;
        int32_t Y = 100;
        uint32_t Width = 600;
        uint32_t Height = 300;
        uint32_t BorderWidth = 1;
        int32_t CreateWindowFlagCount = 2;
        int RequestLength = 8+CreateWindowFlagCount;


#define WINDOWCLASS_COPYFROMPARENT 0
#define WINDOWCLASS_INPUTOUTPUT 1
#define WINDOWCLASS_INPUTONLY 2

#define X11_FLAG_BACKGROUND_PIXEL 0x00000002 
#define X11_FLAG_WIN_EVENT 0x00000800 

        SendBuffer[0] = X11_REQUEST_CREATE_WINDOW;
        SendBuffer[1] = Depth;
        *((int16_t *)&SendBuffer[2]) = RequestLength;
        *((int32_t *)&SendBuffer[4]) = WindowId;
        *((int32_t *)&SendBuffer[8]) = GlobalRootWindow;
        *((int16_t *)&SendBuffer[12]) = X;
        *((int16_t *)&SendBuffer[14]) = Y;
        *((int16_t *)&SendBuffer[16]) = Width;
        *((int16_t *)&SendBuffer[18]) = Height;
        *((int16_t *)&SendBuffer[20]) = BorderWidth;
        *((int16_t *)&SendBuffer[22]) = WINDOWCLASS_INPUTOUTPUT;
        *((int32_t *)&SendBuffer[24]) = GlobalRootVisualId;
        *((int32_t *)&SendBuffer[28]) = X11_FLAG_WIN_EVENT | X11_FLAG_BACKGROUND_PIXEL;
        *((int32_t *)&SendBuffer[32]) = 0xff000000;
        *((int32_t *)&SendBuffer[36]) = X11_EVENT_FLAG_EXPOSURE | X11_EVENT_FLAG_KEY_PRESS;


        BytesWritten = write(Socket, (char *)&SendBuffer, RequestLength*4);
        printf("Create Window: BytesWritten: %d\n", BytesWritten);


        // ------------------------------ Map Window

        SendBuffer[0] = X11_REQUEST_MAP_WINDOW;
        SendBuffer[1] = 0;
        *((int16_t *)&SendBuffer[2]) = 2;
        *((int32_t *)&SendBuffer[4]) = WindowId;

        BytesWritten = write(Socket, (char *)&SendBuffer, 2*4);
        printf("Map Window: BytesWritten: %d\n", BytesWritten);


        /* ------------------------------------------------------------------------------- */

        struct pollfd PollDescriptors[1] = {};
        PollDescriptors[0].fd = Socket;
        PollDescriptors[0].events = POLLIN;
        int32_t DescriptorCount = 1;

        int32_t IsProgramRunning = 1;
        while(IsProgramRunning){
            int32_t EventCount = poll(PollDescriptors, DescriptorCount, -1);

            if(PollDescriptors[0].revents & POLLERR) {
                printf("------- Error\n");
            }

            if(PollDescriptors[0].revents & POLLHUP) {
                printf("---- Connection close\n");
                IsProgramRunning = 0;
            }

            GetAndProcessReply(PollDescriptors[0].fd);
        }
    }

}

To compile it just run this from terminal: gcc basic.c -o basic

Here is video showing how it looks:

Additional Functionality

Now let’s try to add some extra functionality and see if will be to complex to extend it and see if we can actually see something somewhat useful on the screen.

First of all I will make tiny refactoring and move some code into functions to make code a bit more readable. Specifically we will move intiation, window creation and mapping into their specific functions. The event loop will remain the same for now.

int SetupStatus = X_InitiateConnection(Socket);

if(SetupStatus == 0) {
    // X, Y, Width, Height setup
    int WindowId = X_CreatWindow(Socket, X, Y, Width, Height);
    X_MapWindow(Socket, WindowId);

    // Event loop
}

X_InitiateConnection, X_CreatWindow, X_MapWindow are just condensed code from previous section.

Open font

In the end I am planning to use some text for that we will need to ask for a front from the X server. The drill is pretty much the same thing. Prepare data, stuck into buffer and send it to the server.

void X_OpenFont(i32 Socket, i8 *FontName, i32 FontId) {
    char SendBuffer[16*1024] = {};
    int BytesWritten = 0;
    int BytesRead = 0;

    i32 FontNameLength = strlen((char *)FontName);
    i32 Pad = PAD(FontNameLength);
    int RequestLength = (3 + (FontNameLength + Pad)/4);

    SendBuffer[0] = X11_REQUEST_OPEN_FONT;
    SendBuffer[1] = 0;
    *((u16 *)&SendBuffer[2]) = RequestLength;
    *((u32 *)&SendBuffer[4]) = FontId;
    *((u16 *)&SendBuffer[8]) = FontNameLength;
    strncpy(SendBuffer + 12, (char *)FontName, FontNameLength);

    i32 WriteSize = 12 + FontNameLength + Pad;
    BytesWritten = write(Socket, (char *)&SendBuffer, WriteSize);
}

X11 Initialization
request

FontId is something that we as client generate ourself. For that we have GetNextId(). We pass it and a font name to the function requesting server to create font resource with specified ID and try to find closest match to the name we provided.

Create Graphic Context (GC)

A lot of window operations require graphic context to do it’s operations. So we need to get it before we can do any graphic changing actions.

void X_CreateGC(int32_t Socket, int32_t GcId, int32_t FontId) {
    char SendBuffer[16*1024] = {};

    int32_t CreateGcFlagCount = 3;
    int RequestLength = 4 + CreateGcFlagCount;

    SendBuffer[0] = X11_REQUEST_CREATE_GC;
    SendBuffer[1] = 0;
    *((int16_t *)&SendBuffer[2]) = RequestLength;
    *((int32_t *)&SendBuffer[4]) = GcId;
    *((int32_t *)&SendBuffer[8]) = GlobalRootWindow;
    *((int32_t *)&SendBuffer[12]) = X11_FLAG_FG | X11_FLAG_BG | X11_FLAG_FONT;
    *((int32_t *)&SendBuffer[16]) = 0xFF00FF00; // Foreground
    *((int32_t *)&SendBuffer[20]) = 0xFF000000; // Background
    *((int32_t *)&SendBuffer[24]) = FontId; // Font

    write(Socket, (char *)&SendBuffer, RequestLength*4);
}

X11 Initialization
request

Here we see similar patter of passing parameters based on mask and it’s bit offsets. We have required 16 bytes of setup data and extra variable bytes based on how much data we want to set. In this example we are setting just font, background and forground colors. Thus we set X11_FLAG_FG | X11_FLAG_BG | X11_FLAG_FONT mask bits and pass appropriate parameters at the end of the request. There are quite a few parameters that can be passed like different stroke types, drawing functions and etc. But we stick to simplicity for now.

Writing text

The last function for today will be text writing. It is pretty simple.

void WriteText(int Socket, int WindowId, int GCid, i16 X, i16 Y, const char *Text, i32 TextLength) {
    char SendBuffer[16*1024] = {};

    u32 ContentLength = 4 + (TextLength + PAD(TextLength))/4;

    SendBuffer[0] = (u8)X11_REQUEST_IMAGE_TEXT_8;
    SendBuffer[1] = TextLength;
    *((i16 *)&SendBuffer[2]) = ContentLength; 
    *((i32 *)&SendBuffer[4]) = WindowId;
    *((i32 *)&SendBuffer[8]) = GCid;
    *((i16 *)&SendBuffer[12]) = X; 
    *((i16 *)&SendBuffer[14]) = Y; 

    strncpy(&SendBuffer[16], (char *)Text, TextLength);
    write(Socket, (char *)&SendBuffer, ContentLength*4);
}

X11 Initialization
request

We repeated this drill so many times that it should be trivial by now. Calculating length in 4 byte blocks. Set required where we want to draw (window), which brush to draw with (graphic context) and X,Y offsets in the window where we want to draw. At the of request copy the text that we want to draw. And that is it.

The only thing left if draw this text on ech refresh. Here for simplicity purposes we will be drawing text on each event but correct approach would have been to draw it on Expose events. But the text already became too be big so simplicity wins today.

So in the main loop we add this code:

const char* t1 = "Hello, World!";
const char* t2 = "This is a test text directly written to X";
const char* t3 = "Whooha. Is this even legal? Let's keep a secret!";

WriteText(Socket, WindowId, GcId, 10, 20, t1, strlen(t1));
WriteText(Socket, WindowId, GcId, 10, 35, t2, strlen(t2));
WriteText(Socket, WindowId, GcId, 10, 50, t3, strlen(t3));

And that is it! Hey we build a simple window with text. Congratulations.

Here is full source code which contains a bit more functionality than discussed in post (text movement and more detailed debug info):

Final code

#include <sys/socket.h>
#include <sys/un.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>


int32_t GlobalId = 0;
int32_t GlobalIdBase = 0;
int32_t GlobalIdMask = 0;
int32_t GlobalRootWindow = 0;
int32_t GlobalRootVisualId = 0;

int32_t GlobalTextOffsetX = 10;
int32_t GlobalTextOffsetY = 20;

#define READ_BUFFER_SIZE 16*1024

#define RESPONSE_STATE_FAILED 0
#define RESPONSE_STATE_SUCCESS 1
#define RESPONSE_STATE_AUTHENTICATE 2

#define X11_REQUEST_CREATE_WINDOW 1
#define X11_REQUEST_MAP_WINDOW 8
#define X11_REQUEST_IMAGE_TEXT_8 76
#define X11_REQUEST_OPEN_FONT 45
#define X11_REQUEST_CREATE_GC 55


#define X11_EVENT_FLAG_KEY_PRESS 0x00000001
#define X11_EVENT_FLAG_KEY_RELEASE 0x00000002
#define X11_EVENT_FLAG_EXPOSURE 0x8000


#define WINDOWCLASS_COPYFROMPARENT 0
#define WINDOWCLASS_INPUTOUTPUT 1
#define WINDOWCLASS_INPUTONLY 2

#define X11_FLAG_BACKGROUND_PIXEL 0x00000002 
#define X11_FLAG_WIN_EVENT 0x00000800 

#define X11_FLAG_FG 0x00000004
#define X11_FLAG_BG 0x00000008
#define X11_FLAG_FONT 0x00004000
#define X11_FLAG_GC_EXPOSURE 0x00010000

#define PAD(N) ((4 - (N % 4)) % 4)

void VerifyOrDie(int IsSuccess, const char *Message) {
    if(!IsSuccess)  {
        fprintf(stderr, "%s", Message);
        exit(13);
    }
}

void VerifyOrDieWidthErrno(int IsSuccess, const char *Message) {
    if(!IsSuccess)  {
        perror(Message);
        exit(13);
    }
}

void DumpResponseError(int Socket, char* ReadBuffer) {
        uint8_t ReasonLength = ReadBuffer[1];
        uint16_t MajorVersion = *((uint16_t*)&ReadBuffer[2]);
        uint16_t MinorVersion = *((uint16_t*)&ReadBuffer[4]);
        uint16_t AdditionalDataLength = *((uint16_t*)&ReadBuffer[6]); // Length in 4-byte units of "additional data"
        uint8_t *Message = (uint8_t*)&ReadBuffer[8];

        int BytesRead = read(Socket, ReadBuffer + 8, READ_BUFFER_SIZE-8);

        printf("State: %d\n", ReadBuffer[0]);
        printf("MajorVersion: %d\n", MajorVersion);
        printf("MinorVersion: %d\n", MinorVersion);
        printf("AdditionalDataLength: %d\n", AdditionalDataLength);
        printf("Reason: %s\n", Message);
}

void AuthenticateX11() {
    fprintf(stderr, "Current version of the app does not support authentication.\n");
    fprintf(stderr, "Please run 'xhost +local:' in your terminal to disable cookie based authentication\n");
    fprintf(stderr, "and allow local apps to communication with Xorg without it.");
}

int32_t GetNextId() {
    int32_t Result = (GlobalIdMask & GlobalId) | GlobalIdBase;
    GlobalId += 1;
    return Result;
}

void PrintResponseError(char *Data, int32_t Size) {
    char ErrorCode = Data[1];
    const char *ErrorNames[] = {
        "Unknown Error",
        "Request",
        "Value",
        "Window",
        "Pixmap",
        "Atom",
        "Cursor",
        "Font",
        "Match",
        "Drawable",
        "Access",
        "Alloc",
        "Colormap",
        "GContext",
        "IDChoice",
        "Name",
        "Length",
        "Implementation",
    };

    const char* ErrorName = "Unknown error";
    if(ErrorCode < sizeof(ErrorNames) / sizeof(ErrorNames[0])) {
        ErrorName = ErrorNames[ErrorCode];
    }

    
    uint16_t Minor = *((uint16_t*)&Data[8]);
    uint8_t Major = *((uint8_t*)&Data[10]);

    printf("\033[0;31m");
    printf("Response Error: [%d] %s", ErrorCode, ErrorName);
    printf("	Minor: %d, Major: %d", Minor, Major);
    printf("\033[0m\n");


}

void PrintAndProcessEvent(char *Data, int32_t Size) {
    char EventCode = Data[0];
    const char* EventNames[] = {
        "-- Wrong Event Code --",
        "-- Wrong Event Code --",
        "KeyPress",
        "KeyRelease",
        "ButtonPress",
        "ButtonRelease",
        "MotionNotify",
        "EnterNotify",
        "LeaveNotify",
        "FocusIn",
        "FocusOut",
        "KeymapNotify",
        "Expose",
        "GraphicsExposure",
        "NoExposure",
        "VisibilityNotify",
        "CreateNotify",
        "DestroyNotify",
        "UnmapNotify",
        "MapNotify",
        "MapRequest",
        "ReparentNotify",
        "ConfigureNotify",
        "ConfigureRequest",
        "GravityNotify",
        "ResizeRequest",
        "CirculateNotify",
        "CirculateRequest",
        "PropertyNotify",
        "SelectionClear",
        "SelectionRequest",
        "SelectionNotify",
        "ColormapNotify",
        "ClientMessage",
        "MappingNotify",
    };

#define REPLY_EVENT_CODE_KEY_PRESS 2
#define REPLY_EVENT_CODE_EXPOSE 12

const char* TERMINAL_TEXT_COLOR_RED = "\033[0;32m";
const char* TERMINAL_TEXT_COLOR_CLEAR = "\033[0m";

    if(EventCode == REPLY_EVENT_CODE_EXPOSE) {
        // NOTE: Exposure event
        const char *EventName = "Expose";
        uint16_t SequenceNumber = *((uint16_t*)&Data[2]);
        uint32_t Window = *((uint32_t*)&Data[4]);
        uint16_t X = *((uint16_t*)&Data[8]);
        uint16_t Y = *((uint16_t*)&Data[10]);
        uint16_t Width = *((uint16_t*)&Data[12]);
        uint16_t Height = *((uint16_t*)&Data[14]);
        uint16_t Count = *((uint16_t*)&Data[16]);

        printf(TERMINAL_TEXT_COLOR_RED);
            printf("%s: ", EventName);
        printf(TERMINAL_TEXT_COLOR_CLEAR);

        printf("Seq %d, ", SequenceNumber);
        printf("Win %d: ", Window);
        printf("X %d: ", X);
        printf("Y %d: ", Y);
        printf("Width %d: ", Width);
        printf("Height %d: ", Height);
        printf("Count %d: ", Count);
        printf("\n");
        /* printf("%s: Seq %d\n", EventName, SequenceNumber); */
    } else if(EventCode == REPLY_EVENT_CODE_KEY_PRESS) {
        const char *EventName = "KeyPress";
        char KeyCode = Data[1];
        uint16_t SequenceNumber = *((uint16_t*)&Data[2]);
        uint32_t TimeStamp = *((uint32_t*)&Data[4]);
        uint32_t RootWindow = *((uint32_t*)&Data[8]);
        uint32_t EventWindow = *((uint32_t*)&Data[12]);
        uint32_t ChildWindow = *((uint32_t*)&Data[16]); // NOTE: Always 0
        int16_t RootX = *((int16_t*)&Data[20]);
        int16_t RootY = *((int16_t*)&Data[22]);
        int16_t EventX = *((int16_t*)&Data[24]);
        int16_t EventY = *((int16_t*)&Data[26]);
        int16_t SetOfKeyButMask = *((int16_t*)&Data[28]);
        int8_t IsSameScreen = *((int8_t*)&Data[30]);

        printf(TERMINAL_TEXT_COLOR_RED);
            printf("%s: ", EventName);
        printf(TERMINAL_TEXT_COLOR_CLEAR);

        // NOTE: Temporary hack that will not work everywhere
        int StepSize = 10;
        if(KeyCode == 25) { GlobalTextOffsetY += StepSize; }
        if(KeyCode == 39) { GlobalTextOffsetY -= StepSize; }
        if(KeyCode == 38) { GlobalTextOffsetX -= StepSize; }
        if(KeyCode == 40) { GlobalTextOffsetX += StepSize; }

        printf("Code %u, ", (uint8_t)KeyCode);
        printf("Seq %d, ", SequenceNumber);
        printf("Time %d, ", TimeStamp);
        printf("Root %d, ", RootWindow);
        printf("EventW %d, ", EventWindow);
        printf("Child %d, ", ChildWindow);
        printf("RX %d, ", RootX);
        printf("RY %d, ", RootY);
        printf("EX %d, ", EventX);
        printf("EY %d, ", EventY);
        printf("\n");
    } else {
        const char* EventName = " - Unknown Event Code -";
        if(EventCode < sizeof(EventNames) / sizeof(EventNames[0])) {
            EventName = EventNames[EventCode];
        }
        // printf("-------------Event: %s\n", EventName);
        // for(int i = 0; i < Size; i++) {
            // printf("%c", Data[i]);
        // }
        // printf("\n");
    }

}

void GetAndProcessReply(int Socket) {
    char Buffer[1024] = {};
    int32_t BytesRead = read(Socket, Buffer, 1024);

    uint8_t Code = Buffer[0];

    if(Code == 0) {
        PrintResponseError(Buffer, BytesRead);
    } else if (Code == 1) {
        printf("---------------- Unexpected reply\n");
    } else {
        // NOTE: Event?
        PrintAndProcessEvent(Buffer, BytesRead);
    }
}

int X_InitiateConnection(int Socket) { 
    // TODO: Remove global variables and put them into 'connection' struct.
    int SetupStatus = 1;
    char SendBuffer[16*1024] = {};
    char ReadBuffer[16*1024] = {};

    uint8_t InitializationRequest[12] = {};
    InitializationRequest[0] = 'l';
    InitializationRequest[1] = 0;
    InitializationRequest[2] = 11;

    int BytesWritten = write(Socket, (char*)&InitializationRequest, sizeof(InitializationRequest));
    VerifyOrDie(BytesWritten == sizeof(InitializationRequest), "Wrong amount of bytes written during initialization");

    int BytesRead = read(Socket, ReadBuffer, 8);

    if(ReadBuffer[0] == RESPONSE_STATE_FAILED) {
        DumpResponseError(Socket, ReadBuffer);
    }
    else if(ReadBuffer[0] == RESPONSE_STATE_AUTHENTICATE) {
        AuthenticateX11();
    }
    else if(ReadBuffer[0] == RESPONSE_STATE_SUCCESS) {
        printf("INIT Response SUCCESS. BytesRead: %d\n", BytesRead);

        BytesRead = read(Socket, ReadBuffer + 8, READ_BUFFER_SIZE-8);
        printf("---------------------------%d\n", BytesRead);

        /* -------------------------------------------------------------------------------- */
        uint8_t _Unused = ReadBuffer[1];
        uint16_t MajorVersion = *((uint16_t*)&ReadBuffer[2]);
        uint16_t MinorVersion = *((uint16_t*)&ReadBuffer[4]);
        uint16_t AdditionalDataLength = *((uint16_t*)&ReadBuffer[6]); // Length in 4-byte units of "additional data"

        uint32_t ResourceIdBase = *((uint32_t*)&ReadBuffer[12]);
        uint32_t ResourceIdMask = *((uint32_t*)&ReadBuffer[16]);
        uint16_t LengthOfVendor = *((uint16_t*)&ReadBuffer[24]);
        uint8_t NumberOfFormants = *((uint16_t*)&ReadBuffer[29]);
        uint8_t *Vendor = (uint8_t *)&ReadBuffer[40];

        int32_t VendorPad = PAD(LengthOfVendor);
        int32_t FormatByteLength = 8 * NumberOfFormants;
        int32_t ScreensStartOffset = 40 + LengthOfVendor + VendorPad + FormatByteLength;

        uint32_t RootWindow = *((uint32_t*)&ReadBuffer[ScreensStartOffset]);
        uint32_t RootVisualId = *((uint32_t*)&ReadBuffer[ScreensStartOffset + 32]);

        GlobalIdBase = ResourceIdBase;
        GlobalIdMask = ResourceIdMask;
        GlobalRootWindow = RootWindow;
        GlobalRootVisualId = RootVisualId;

        SetupStatus = 0;
    }

    return SetupStatus;
}

int X_CreatWindow(int Socket, int X, int Y, int Width, int Height) {
    // TODO: Put this into 'connection' struct
    char SendBuffer[16*1024] = {};
    char ReadBuffer[16*1024] = {};

    int32_t WindowId = GetNextId();
    int32_t Depth = 0;
    uint32_t BorderWidth = 1;
    int32_t CreateWindowFlagCount = 2;
    int RequestLength = 8+CreateWindowFlagCount;

    SendBuffer[0] = X11_REQUEST_CREATE_WINDOW;
    SendBuffer[1] = Depth;
    *((int16_t *)&SendBuffer[2]) = RequestLength;
    *((int32_t *)&SendBuffer[4]) = WindowId;
    *((int32_t *)&SendBuffer[8]) = GlobalRootWindow;
    *((int16_t *)&SendBuffer[12]) = X;
    *((int16_t *)&SendBuffer[14]) = Y;
    *((int16_t *)&SendBuffer[16]) = Width;
    *((int16_t *)&SendBuffer[18]) = Height;
    *((int16_t *)&SendBuffer[20]) = BorderWidth;
    *((int16_t *)&SendBuffer[22]) = WINDOWCLASS_INPUTOUTPUT;
    *((int32_t *)&SendBuffer[24]) = GlobalRootVisualId;
    *((int32_t *)&SendBuffer[28]) = X11_FLAG_WIN_EVENT | X11_FLAG_BACKGROUND_PIXEL;
    *((int32_t *)&SendBuffer[32]) = 0xff000000;
    *((int32_t *)&SendBuffer[36]) = X11_EVENT_FLAG_EXPOSURE | X11_EVENT_FLAG_KEY_PRESS;

    int BytesWritten = write(Socket, (char *)&SendBuffer, RequestLength*4);

    return WindowId;
}

int X_MapWindow(int Socket, int WindowId) {
    // TODO: Put this into 'connection' struct
    char SendBuffer[16*1024] = {};
    char ReadBuffer[16*1024] = {};

    SendBuffer[0] = X11_REQUEST_MAP_WINDOW;
    SendBuffer[1] = 0;
    *((int16_t *)&SendBuffer[2]) = 2;
    *((int32_t *)&SendBuffer[4]) = WindowId;

    int BytesWritten = write(Socket, (char *)&SendBuffer, 2*4);
    return 0;
}

void X_OpenFont(int32_t Socket, char *FontName, int32_t FontId) {
    char SendBuffer[16*1024] = {};
    char ReadBuffer[16*1024] = {};
    int BytesWritten = 0;
    int BytesRead = 0;

    int32_t FontNameLength = strlen((char *)FontName);
    int32_t Pad = PAD(FontNameLength);
    int RequestLength = (3 + (FontNameLength + Pad)/4);

    SendBuffer[0] = X11_REQUEST_OPEN_FONT;
    SendBuffer[1] = 0;
    *((uint16_t *)&SendBuffer[2]) = RequestLength;
    *((uint32_t *)&SendBuffer[4]) = FontId;
    *((uint16_t *)&SendBuffer[8]) = FontNameLength;
    strncpy(SendBuffer + 12, (char *)FontName, FontNameLength);

    int32_t WriteSize = 12 + FontNameLength + Pad;
    BytesWritten = write(Socket, (char *)&SendBuffer, WriteSize);
}

void X_CreateGC(int32_t Socket, int32_t GcId, int32_t FontId) {
    char SendBuffer[16*1024] = {};

    int32_t CreateGcFlagCount = 3;
    int RequestLength = 4 + CreateGcFlagCount;

    SendBuffer[0] = X11_REQUEST_CREATE_GC;
    SendBuffer[1] = 0;
    *((int16_t *)&SendBuffer[2]) = RequestLength;
    *((int32_t *)&SendBuffer[4]) = GcId;
    *((int32_t *)&SendBuffer[8]) = GlobalRootWindow;
    *((int32_t *)&SendBuffer[12]) = X11_FLAG_FG | X11_FLAG_BG | X11_FLAG_FONT;
    *((int32_t *)&SendBuffer[16]) = 0xFF00FF00; // Foreground
    *((int32_t *)&SendBuffer[20]) = 0xFF000000; // Background
    *((int32_t *)&SendBuffer[24]) = FontId; // Font

    write(Socket, (char *)&SendBuffer, RequestLength*4);
}

void WriteText(int Socket, int WindowId, int GCid, int16_t X, int16_t Y, const char *Text, int32_t TextLength) {
    char Buffer[16*1024] = {};

    uint32_t ContentLength = 4 + (TextLength + PAD(TextLength))/4;

    Buffer[0] = (uint8_t)X11_REQUEST_IMAGE_TEXT_8;
    Buffer[1] = TextLength;
    *((int16_t *)&Buffer[2]) = ContentLength; 
    *((int32_t *)&Buffer[4]) = WindowId;
    *((int32_t *)&Buffer[8]) = GCid;
    *((int16_t *)&Buffer[12]) = X; 
    *((int16_t *)&Buffer[14]) = Y; 

    strncpy(&Buffer[16], (char *)Text, TextLength);
    int BytesWritten = write(Socket, (char *)&Buffer, ContentLength*4);
}

int main(){
    int Socket = socket(AF_UNIX, SOCK_STREAM, 0);
    VerifyOrDie(Socket > 0, "Couldn't open a socket(...)");

    struct sockaddr_un Address;
    memset(&Address, 0, sizeof(struct sockaddr_un));
    Address.sun_family = AF_UNIX;
    strncpy(Address.sun_path, "/tmp/.X11-unix/X0", sizeof(Address.sun_path)-1);

    int Status = connect(Socket, (struct sockaddr *)&Address, sizeof(Address));
    VerifyOrDieWidthErrno(Status == 0, "Couldn't connect to a unix socket with connect(...)");

    int SetupStatus = X_InitiateConnection(Socket);

    if(SetupStatus == 0) {
        int32_t X = 100;
        int32_t Y = 100;
        uint32_t Width = 600;
        uint32_t Height = 300;
        int WindowId = X_CreatWindow(Socket, X, Y, Width, Height);

        X_MapWindow(Socket, WindowId);

        int32_t FontId = GetNextId();
        X_OpenFont(Socket, (int8_t *)"fixed", FontId);

        int32_t GcId = GetNextId();
        X_CreateGC(Socket, GcId, FontId);

        struct pollfd PollDescriptors[1] = {};
        PollDescriptors[0].fd = Socket;
        PollDescriptors[0].events = POLLIN;
        int32_t DescriptorCount = 1;
        int32_t IsProgramRunning = 1;
        while(IsProgramRunning){
            int32_t EventCount = poll(PollDescriptors, DescriptorCount, -1);

            if(PollDescriptors[0].revents & POLLERR) {
                printf("------- Error\n");
            }

            if(PollDescriptors[0].revents & POLLHUP) {
                printf("---- Connection close\n");
                IsProgramRunning = 0;
            }

            char* t1 = "Hello, World!";
            char* t2 = "This is a test text directly written to X";
            char* t3 = "Whooha. Is this even legal? Let's keep a secret!";
            WriteText(Socket, WindowId, GcId, GlobalTextOffsetX, GlobalTextOffsetY, t1, strlen(t1));
            WriteText(Socket, WindowId, GcId, GlobalTextOffsetX, GlobalTextOffsetY + 15, t2, strlen(t2));
            WriteText(Socket, WindowId, GcId, GlobalTextOffsetX, GlobalTextOffsetY + 30, t3, strlen(t3));

            GetAndProcessReply(PollDescriptors[0].fd);
        }
    }

}

Compile it with: gcc main.c -o main

Here is how it looks.

Conclusion

X Server is slowly being depricated in the linux world and being replaced Wayland. Still X11 is an interesting protocol to look at from the perspective of binary communication and management of resource which require fast speeds.

In this post I tried to cover basic information and create a simple but working app that is simple, defined in single file and easily compiles. No external code except libc was used. I find it fascinating when you can open black boxes and see how gears move each other. Hope you liked it as well.