Programming in C: Building Your Own AI Assistant

Table of contents
- Why C Programming?
- What Are TCP Sockets?
- Programming with libcurl in C
- Project Overview: Building Jarvis
- Prerequisites
- Step 1: Setting Up the Project Structure
- Step 2: The Configuration File
- Step 3: Setting Up CMake
- Step 4: The Main Code
- Socket Programming Under the Hood
- Building and Running the Project
- Common Issues and How to Fix Them
- Taking It Further
- Conclusion

Welcome to the wonderful world of C programming, where semicolons rule and memory leaks lurk in the shadows!
Have you ever wanted to build your own AI assistant that doesn't spy on you, sell your data, or randomly burst into flames? Well, put on your developer hat (preferably a fedora, for maximum debugging power) because today we're going to create "Jarvis" - a command-line AI assistant using pure C and the magic of TCP socket programming!
Why C Programming?
Because we hate ourselves, obviously! Just kidding (mostly). C gives us control that higher-level languages can only dream about. It's like driving a car with a manual transmission versus an automatic - sure, you might stall a few times and occasionally roll backward into expensive German sedans, but the feeling of control is worth it!
What Are TCP Sockets?
Think of TCP sockets as the postal service of the internet. They establish connections between applications across a network, ensuring reliable, ordered delivery of data. Unlike UDP (which is more like throwing messages into the wind and hoping they reach their destination), TCP makes sure your packets arrive intact and in the correct order.
In our case, we'll use sockets to communicate with OpenAI's servers so we can chat with their AI models. Let's get started!
Programming with libcurl in C
Before diving into our project, let's understand how to use libcurl properly. libcurl is a powerful client-side URL transfer library that handles the low-level socket operations for us. It's like hiring a professional driver instead of manually shifting gears yourself.
The libcurl Lifecycle
Working with libcurl follows a specific lifecycle:
Global Initialization: Start by initializing the libcurl environment
Handle Creation: Create a curl easy handle
Option Setting: Configure the handle with your desired options
Request Performance: Execute the request
Cleanup: Free resources and shutdown the libcurl environment
Step 1: Global Initialization
Always begin by initializing the libcurl environment:
curl_global_init(CURL_GLOBAL_ALL);
This sets up necessary global resources that libcurl needs. The CURL_GLOBAL_ALL
flag initializes everything, including SSL support and socket operations. You could use more specific flags like:
CURL_GLOBAL_SSL
- Just initialize SSLCURL_GLOBAL_WIN32
- Initialize Windows-specific featuresCURL_GLOBAL_NOTHING
- Explicitly initialize nothing
Step 2: Creating a Curl Handle
Next, create a curl easy handle:
CURL *curl = curl_easy_init();
if (!curl) {
fprintf(stderr, "Failed to initialize curl\n");
return 1;
}
This handle is your interface to all curl operations - think of it as your "connection manager."
Step 3: Setting Options
libcurl's power comes from its extensive option system. You configure your connection through these options:
// Set the URL
curl_easy_setopt(curl, CURLOPT_URL, "https://api.example.com/endpoint");
// Set SSL verification (CRITICAL for security!)
curl_easy_setopt(curl, CURLOPT_CAINFO, "cacert.pem");
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
// Set headers
struct curl_slist *headers = NULL;
headers = curl_slist_append(headers, "Content-Type: application/json");
headers = curl_slist_append(headers, "Authorization: Bearer YOUR_TOKEN");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
// Set request type (GET is default)
curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); // For GET
// Or for POST:
curl_easy_setopt(curl, CURLOPT_POST, 1L);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "your-post-data");
// Set timeouts
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); // 30 seconds timeout
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L); // 10 seconds connect timeout
Step 4: Handling Response Data
Unlike simpler libraries, libcurl doesn't automatically store response data. You need to provide a callback function that processes the incoming data:
// Structure to store the response
struct MemoryStruct {
char *memory;
size_t size;
};
// Callback function to handle received data
static size_t WriteMemoryCallback(void *contents, size_t size, size_t nmemb, void *userp) {
size_t realsize = size * nmemb;
struct MemoryStruct *mem = (struct MemoryStruct *)userp;
// Reallocate memory to fit the new data
char *ptr = realloc(mem->memory, mem->size + realsize + 1);
if(!ptr) {
fprintf(stderr, "Out of memory!\n");
return 0; // Signal error to libcurl
}
// Store the new data
mem->memory = ptr;
memcpy(&(mem->memory[mem->size]), contents, realsize);
mem->size += realsize;
mem->memory[mem->size] = 0; // Null terminate
return realsize; // Return the size of processed data
}
// Set up the callback
struct MemoryStruct chunk;
chunk.memory = malloc(1); // Initialize with a small size
chunk.size = 0;
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&chunk);
This callback is where the socket data is actually processed. libcurl receives data from the socket and passes it to your callback function.
Step 5: Performing the Request
Now execute the request:
CURLcode res = curl_easy_perform(curl);
if(res != CURLE_OK) {
fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
}
This is where all the TCP socket magic happens behind the scenes:
libcurl opens a socket connection
Performs the SSL/TLS handshake if using HTTPS
Sends your HTTP headers and data
Receives the response and passes it to your callback
Manages error conditions and retries if needed
Step 6: Getting Response Information
You can extract useful information about the response:
// Get HTTP response code
long http_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
printf("HTTP Response Code: %ld\n", http_code);
// Get content type
char *content_type = NULL;
curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &content_type);
printf("Content-Type: %s\n", content_type);
Step 7: Proper Cleanup
Always clean up to prevent memory leaks:
// Free the header list
curl_slist_free_all(headers);
// Clean up the curl handle
curl_easy_cleanup(curl);
// Global cleanup
curl_global_cleanup();
// Free your memory structure
free(chunk.memory);
Thread Safety Considerations
libcurl is thread-safe with some caveats:
curl_global_init()
andcurl_global_cleanup()
are not thread-safeEach thread should use its own curl handle
Never share handles between threads without proper locking
Debug Mode for Troubleshooting
When troubleshooting, enable debug mode to see what's happening under the hood:
// Set debug function
curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, my_debug_callback);
// Debug callback function
static int my_debug_callback(CURL *handle, curl_infotype type, char *data, size_t size, void *userp) {
// Print different types of debug info
switch(type) {
case CURLINFO_TEXT:
fprintf(stderr, "* %s", data);
break;
case CURLINFO_HEADER_OUT:
fprintf(stderr, "=> Send header: %.*s", (int)size, data);
break;
case CURLINFO_DATA_OUT:
fprintf(stderr, "=> Send data: %zu bytes\n", size);
break;
// And so on for other info types...
}
return 0;
}
Now let's apply these principles to our AI assistant project!
Project Overview: Building Jarvis
Our goal is to create a simple C program that:
Takes a question as input
Connects to OpenAI's API using TCP sockets (via libcurl)
Sends our question to the API
Gets a response from the AI
Extracts and displays the response
Prerequisites
Before we begin our journey into the C programming abyss, you'll need:
A C compiler (GCC, Clang, MSVC)
CMake for building
libcurl for handling HTTP requests
cJSON for parsing JSON
An OpenAI API key
CA certificate bundle (cacert.pem) for SSL verification
For the CA certificate bundle, you'll need to download cacert.pem
from curl.se/docs/caextract.html. This file contains trusted root certificates needed for secure HTTPS connections. Without it, your program won't be able to verify the SSL certificate of the OpenAI API, and you'll either get connection errors or be vulnerable to man-in-the-middle attacks - neither of which is particularly fun!
Step 1: Setting Up the Project Structure
First, let's set up our project. Create a directory for our project and the following files:
jarvis/
├── main.c
├── cJSON.c
├── cJSON.h
├── CMakeLists.txt
├── jarvis.conf
└── cacert.pem (for SSL verification)
You can download cJSON from GitHub and libcurl from curl.se for Windows or using your package manager on Linux.
Make sure to place the cacert.pem
file in your project directory. This is crucial for establishing secure connections - think of it as your program's "list of trusted friends" on the internet.
Step 2: The Configuration File
Let's start with something simple - our configuration file. Create a file named jarvis.conf
:
OPENAI_API_KEY=sk-proj-...
MODEL=gpt-4o-mini
DEBUG=true
Replace the API key with your own. This is where we'll store our OpenAI API key and model preference. Setting DEBUG to true will help us see what's happening behind the scenes - like watching the sausage get made, but for HTTP requests!
Step 3: Setting Up CMake
Let's create our CMakeLists.txt file. This is like a recipe for building our program:
For Windows:
cmake_minimum_required(VERSION 3.22)
project(jarvis C)
set(CMAKE_C_STANDARD 17)
# Curl Paths
set(CURL_INCLUDE_DIR "D:/Libraries/curl-8.13.0_2-win64-mingw/include")
set(CURL_LIB_DIR "D:/Libraries/curl-8.13.0_2-win64-mingw/lib")
# Include directories
include_directories(${CURL_INCLUDE_DIR})
# Library directories
link_directories(${CURL_LIB_DIR})
# Add cJSON source files
add_library(cjson STATIC cJSON.c)
add_executable(jarvis main.c)
# When using MinGW curl on Windows, the library name is typically libcurl.dll.a or libcurl.a
target_link_libraries(jarvis
libcurl # or might be libcurl.a or just curl
cjson
)
For Linux:
cmake_minimum_required(VERSION 3.22)
project(jarvis C)
set(CMAKE_C_STANDARD 17)
# Find libcurl package
find_package(CURL REQUIRED)
# Include directories
include_directories(${CURL_INCLUDE_DIRS})
# Add cJSON source files
add_library(cjson STATIC cJSON.c)
add_executable(jarvis main.c)
# Link libraries
target_link_libraries(jarvis
${CURL_LIBRARIES}
cjson
)
On Linux, we use find_package()
instead of setting paths manually because we're civilized people who use package managers!
Step 4: The Main Code
Now for the fun part - the actual C code! Let's break down the main components:
Headers and Definitions
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <curl/curl.h>
#include "cJSON.h"
#define CONFIG_FILE "jarvis.conf"
// Structure to hold response data
struct MemoryStruct {
char *memory;
size_t size;
};
// Global debug flag
int debug_enabled = 0;
Here we're including necessary headers and defining a structure to store our HTTP response. We're using a global debug flag because we like to live dangerously! (Actually, it's for convenience, but don't tell anyone).
Helper Functions
Next, let's implement some helper functions:
// Function to extract and print the assistant's response from the JSON
void extract_and_print_content(const char *json_string) {
cJSON *root = cJSON_Parse(json_string);
if (!root) {
fprintf(stderr, "Error parsing JSON: %s\n", cJSON_GetErrorPtr());
return;
}
// Navigate through the JSON structure
cJSON *choices = cJSON_GetObjectItem(root, "choices");
if (choices && cJSON_IsArray(choices) && cJSON_GetArraySize(choices) > 0) {
cJSON *first_choice = cJSON_GetArrayItem(choices, 0);
if (first_choice) {
cJSON *message = cJSON_GetObjectItem(first_choice, "message");
if (message) {
cJSON *content = cJSON_GetObjectItem(message, "content");
if (content && cJSON_IsString(content)) {
// Print just the content
printf("\n%s\n", content->valuestring);
} else {
fprintf(stderr, "Content not found or not a string\n");
}
} else {
fprintf(stderr, "Message not found\n");
}
} else {
fprintf(stderr, "First choice not found\n");
}
} else {
fprintf(stderr, "Choices not found or empty\n");
}
// Clean up
cJSON_Delete(root);
}
// Parse boolean value from string
int parse_bool(const char* value) {
if (!value) return 0;
if (strcmp(value, "1") == 0 ||
strcasecmp(value, "true") == 0) {
return 1;
}
return 0;
}
char* read_config(const char* key) {
static char line[512];
FILE* file = fopen(CONFIG_FILE, "r");
if (!file) {
perror("Config file");
exit(1);
}
while (fgets(line, sizeof(line), file)) {
line[strcspn(line, "\r\n")] = 0;
char* eq = strchr(line, '=');
if (!eq) continue;
*eq = '\0';
char* value = eq + 1;
if (strcmp(line, key) == 0) {
// Special handling for DEBUG property
if (strcmp(key, "DEBUG") == 0) {
debug_enabled = parse_bool(value);
fclose(file);
return _strdup(value);
}
fclose(file);
return _strdup(value);
}
}
fclose(file);
fprintf(stderr, "Error: Key '%s' not found in %s\n", key, CONFIG_FILE);
exit(1);
}
// Function to escape JSON string
char* escape_json_string(const char* input) {
if (!input) return NULL;
size_t input_len = strlen(input);
// Allocate enough memory for worst case scenario (every char needs escaping)
char* result = (char*)malloc(input_len * 2 + 1);
if (!result) return NULL;
size_t j = 0;
for (size_t i = 0; i < input_len; i++) {
switch (input[i]) {
case '\"':
result[j++] = '\\';
result[j++] = '\"';
break;
case '\\':
result[j++] = '\\';
result[j++] = '\\';
break;
case '\b':
result[j++] = '\\';
result[j++] = 'b';
break;
case '\f':
result[j++] = '\\';
result[j++] = 'f';
break;
case '\n':
result[j++] = '\\';
result[j++] = 'n';
break;
case '\r':
result[j++] = '\\';
result[j++] = 'r';
break;
case '\t':
result[j++] = '\\';
result[j++] = 't';
break;
default:
result[j++] = input[i];
}
}
result[j] = '\0';
return result;
}
The escape_json_string
function escapes special characters in our input string so it plays nicely with JSON. Without this, a single backslash or quote mark could cause our JSON to explode in a spectacular fashion - the programming equivalent of adding water to a deep fryer!
libcurl Callback Functions
Now let's add our callback functions for libcurl:
// Callback function for writing received data
static size_t WriteMemoryCallback(void *contents, size_t size, size_t nmemb, void *userp) {
size_t realsize = size * nmemb;
struct MemoryStruct *mem = (struct MemoryStruct *)userp;
char *ptr = realloc(mem->memory, mem->size + realsize + 1);
if(!ptr) {
fprintf(stderr, "Not enough memory (realloc returned NULL)\n");
return 0;
}
mem->memory = ptr;
memcpy(&(mem->memory[mem->size]), contents, realsize);
mem->size += realsize;
mem->memory[mem->size] = 0;
return realsize;
}
// Debug function to print libcurl verbose information
static int my_trace(CURL *handle, curl_infotype type, char *data, size_t size, void *userp) {
// Skip printing if debug is not enabled
if (!debug_enabled) return 0;
(void)handle; /* prevent compiler warning */
(void)userp;
const char *text;
switch(type) {
case CURLINFO_TEXT:
fprintf(stderr, "* %s", data);
break;
case CURLINFO_HEADER_OUT:
text = "=> Send header";
fprintf(stderr, "%s\n%.*s\n", text, (int)size, data);
break;
case CURLINFO_DATA_OUT:
text = "=> Send data";
fprintf(stderr, "%s (%zu bytes)\n", text, size);
break;
case CURLINFO_SSL_DATA_OUT:
text = "=> Send SSL data";
fprintf(stderr, "%s (%zu bytes)\n", text, size);
break;
case CURLINFO_HEADER_IN:
text = "<= Recv header";
fprintf(stderr, "%s\n%.*s\n", text, (int)size, data);
break;
case CURLINFO_DATA_IN:
text = "<= Recv data";
fprintf(stderr, "%s (%zu bytes)\n", text, size);
break;
case CURLINFO_SSL_DATA_IN:
text = "<= Recv SSL data";
fprintf(stderr, "%s (%zu bytes)\n", text, size);
break;
default: /* in case a new one is introduced to shock us */
return 0;
}
return 0;
}
The WriteMemoryCallback
function is where the magic of socket programming happens behind the scenes. When data comes in through our TCP connection, libcurl calls this function to handle the incoming bytes. We're just making sure we have enough memory to store it all - like preparing a big enough bucket to catch rainwater.
The Main Function
Finally, the main function that ties everything together:
int main(int argc, char* argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s \"your question\"\n", argv[0]);
return 1;
}
CURL *curl;
CURLcode res;
const char* question = argv[1];
char* api_key = NULL;
char* model = NULL;
char* debug_setting = NULL;
char* escaped_question = NULL;
struct MemoryStruct chunk;
struct curl_slist *headers = NULL;
int ret_val = 1;
// Initialize memory chunk
chunk.memory = malloc(1);
chunk.size = 0;
// Read configuration
api_key = read_config("OPENAI_API_KEY");
model = read_config("MODEL");
debug_setting = read_config("DEBUG"); // Read DEBUG setting
// Escape the question for JSON
escaped_question = escape_json_string(question);
if (!escaped_question) {
fprintf(stderr, "Failed to escape the question string\n");
goto cleanup;
}
// Initialize curl
curl_global_init(CURL_GLOBAL_ALL);
curl = curl_easy_init();
if (!curl) {
fprintf(stderr, "Failed to initialize curl\n");
goto cleanup;
}
curl_easy_setopt(curl, CURLOPT_CAINFO, "cacert.pem");
// Set the URL for the POST request
curl_easy_setopt(curl, CURLOPT_URL, "https://api.openai.com/v1/chat/completions");
// Set up the auth header
char auth_header[256];
snprintf(auth_header, sizeof(auth_header), "Authorization: Bearer %s", api_key);
headers = curl_slist_append(headers, auth_header);
headers = curl_slist_append(headers, "Content-Type: application/json");
headers = curl_slist_append(headers, "User-Agent: jarvis-c-client/1.0");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
// Build the JSON payload
char* json_payload = NULL;
size_t json_len = 0;
json_len = snprintf(NULL, 0,
"{\"model\":\"%s\",\"messages\":[{\"role\":\"user\",\"content\":\"%s\"}]}",
model, escaped_question) + 1;
json_payload = (char*)malloc(json_len);
if (!json_payload) {
fprintf(stderr, "Failed to allocate memory for JSON payload\n");
goto cleanup;
}
snprintf(json_payload, json_len,
"{\"model\":\"%s\",\"messages\":[{\"role\":\"user\",\"content\":\"%s\"}]}",
model, escaped_question);
// Set the POST fields
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_payload);
// Set up the write callback function
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&chunk);
// Set timeout
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 120L);
// Only set verbose mode if debugging is enabled
if (debug_enabled) {
curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, my_trace);
}
// Set SSL verification options
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
// Only print if debug is enabled
if (debug_enabled) {
printf("Sending request to OpenAI API...\n");
}
// Perform the request
res = curl_easy_perform(curl);
// Check for errors
if(res != CURLE_OK) {
fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
goto cleanup;
} else {
// Get HTTP response code
long http_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
if (debug_enabled) {
printf("HTTP Response Code: %ld\n", http_code);
}
// Print the response
if (debug_enabled) {
printf("\n--- Server Response ---\n");
}
if (chunk.size > 0) {
if (debug_enabled) {
printf("%s\n", chunk.memory);
}
extract_and_print_content(chunk.memory);
if (debug_enabled) {
printf("Total received: %zu bytes\n", chunk.size);
}
} else {
printf("No data received from server.\n");
}
if (debug_enabled) {
printf("--- End of Response ---\n");
}
}
ret_val = 0;
cleanup:
if (debug_enabled) {
printf("Cleaning up...\n");
}
if (escaped_question) free(escaped_question);
if (api_key) free(api_key);
if (model) free(model);
if (debug_setting) free(debug_setting);
if (chunk.memory) free(chunk.memory);
if (json_payload) free(json_payload);
if (headers) curl_slist_free_all(headers);
if (curl) curl_easy_cleanup(curl);
curl_global_cleanup();
return ret_val;
}
Pay special attention to this line:
curl_easy_setopt(curl, CURLOPT_CAINFO, "cacert.pem");
This tells libcurl where to find the SSL certificate authority bundle, which is essential for verifying the identity of the server we're connecting to. Without this, either your connection will fail, or worse, you might connect to an imposter server! It's like checking ID before letting someone into your house.
Socket Programming Under the Hood
Now, you might be thinking, "Wait, where's the socket programming? I don't see any socket()
, connect()
, or recv()
calls!"
Well, my eager apprentice, libcurl abstracts away the raw socket operations for us. Under the hood, it's doing all the dirty work:
Creating a socket with
socket()
Connecting to the server with
connect()
Sending data with
send()
Receiving data with
recv()
Handling SSL/TLS encryption
Managing HTTP headers and response codes
If we were true masochists, we could implement all of this ourselves, but even C programmers have limits to their self-punishment!
Building and Running the Project
On Windows:
mkdir build
cd build
cmake .. -G "MinGW Makefiles"
mingw32-make
On Linux:
mkdir build
cd build
cmake ..
make
Now you can run your new AI assistant:
./jarvis "What's the meaning of life?"
And Jarvis will respond with whatever philosophical musings the AI has to offer!
Common Issues and How to Fix Them
1. "No SSL Support in libcurl" Error
This means your libcurl was compiled without SSL support. You need this to connect to HTTPS URLs. Make sure you download a version with SSL support or recompile with SSL enabled.
2. SSL Certificate Verification Failed
If you see errors about SSL certificate verification, make sure:
The
cacert.pem
file is in the same directory as your executableThe file path in
CURLOPT_CAINFO
is correctThe certificate bundle is up-to-date (certificates do expire!)
You can always download the latest bundle from curl.se/docs/caextract.html.
3. Memory Leaks
If your program starts acting like it has dementia (or Windows starts complaining about memory), check that you're freeing all allocated memory. C doesn't hold your hand with garbage collection - it expects you to clean up your own mess, like a responsible adult.
4. JSON Parsing Failures
If you get JSON parsing errors, check that your input is properly escaped. A single unescaped quote can turn your beautiful JSON into digital spaghetti.
Taking It Further
Now that you've built a basic AI assistant, here are some ways to enhance it:
Add a conversation history to maintain context
Implement streaming responses for real-time feedback
Create a simple GUI (if you're feeling particularly masochistic)
Add voice input/output capabilities
Optimize memory usage for larger responses
Conclusion
Congratulations! You've just built your own AI assistant using C and TCP socket programming (via libcurl). While higher-level languages might let you accomplish this in fewer lines of code, nothing beats the satisfaction of building something from the ground up in C.
Remember, C programming is like riding a bicycle - except the bicycle is on fire, you're on fire, everything is on fire, and you're in hell. But once you get the hang of it, it's an incredibly rewarding skill that gives you deep insight into how computers actually work.
Now go forth and conquer the world with your new C programming powers! Just remember to free your memory, or your computer will hate you forever.
Happy coding!
Author Bio
Rafal Jackiewicz is an author of books about programming in C, C++ and Java. You can find more information about him and his work on Amazon.
P.S. If your computer explodes while running this code, you probably missed a semicolon somewhere. It's ALWAYS a semicolon.
Subscribe to my newsletter
Read articles from Rafal Jackiewicz directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Rafal Jackiewicz
Rafal Jackiewicz
Rafal Jackiewicz is an author of books about programming in C and Java. You can find more information about him and his work on https://www.jackiewicz.org