Before you even start reading, I mean WTF do you want to learn how to do this using this old-ass C programming language? I mean you can do this with like 10 lines max with such silly languages like python and javascript! But no, we like problem!
Then again, why did I create a guide for it at all. Okay, Okay, you caught me. Why do programmers do anything at all?
So get ready as we smash some stack and leak some memories, yaaaaaay!
Introduction
The goal of this guide, is to provide a kind of template you can use if maybe you have "a gun to your head" and someone asks you to make a request to an API, parse the JSON response, and then print particular values to the terminal, or save it to a file. And that you must do it with C.
We'll need two external C libraries that has to be installed on your computer. Using the package manager for your OS (I'm using Ubuntu).
- Libcurl - for making network request from your program.
- Jansson - For parsing JSON like strings. Because ofcourse, the response from our request url is kindda a string as expected.
sudo apt-get update
sudo apt-get install libcurl4-gnutls-dev
sudo apt-get install libjansson-dev
What's the Plan here?
Okay, so we want to build a program that presents the user with a prompt of certain choices. When the user picks an option, we make a GET request to the Quotable API (https://api.quotable.io/random), we then parse the response and print the output to the terminal. If the user, wants the quotes saved to a file, we'll do that also.
Result Printed on Terminal:
Project Structure
I mean, even if it's a very primitive program, we still have to maintain a modular structure for our own sanity. I mean we're not savages! The image below shows the project structure. i.e Files we'll be creating for our program
-
quotes_main.c
: This will be the entry point of the program where we define the loop that gives the user a prompt. We're also using a custom function from utilities.c (print_initial_ui()) to print the intro text.
#include "quotes.h"
/**
* quotes_main.c
* @argc: Commmand line count
* @argv: Command line arguments
* Return - EXIT SUCCESS OR EXIT_FAILURE
*/
int main(int argc, char const **argv)
{
int continue_prog = 1;
char *temp = NULL;
while (continue_prog)
{
print_initial_ui();
char user_option = 'x';
// Retrieve User's choice
scanf(" %c", &user_option);
printf("\n");
system("clear");
// Match the choice to an action
switch (user_option)
{
case '1':
printf("Loading quotes...\n");
handle_single_quote();
break;
case '2':
handle_custom_number_quotes();
break;
case '3':
handle_custom_number_quotes_to_file();
break;
default:
continue_prog = 0;
break;
}
}
printf("\n");
return 0;
}
-
quotes.h
: We have to include the necessary header files we'll be needing for our little project, a type definition, as well as function prototypes for the custom functions we'll be passing around.
#ifndef QUOTES_H
#define QUOTES_H
/**
* Standard Library Imports
*/
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <strings.h>
#include <string.h>
#include <stdlib.h>
#include <stddef.h>
typedef struct
{
char *data;
size_t len;
} string;
#include <curl/curl.h>
#include <jansson.h>
/**
* HeaderFile for Write Function and others
*/
size_t write_callback(char *ptr, size_t size, size_t nmemb, void *data_struct);
void print_fake_app_loading(void);
void print_initial_ui(void);
char *parse_result(char *result);
void handle_single_quote();
void handle_custom_number_quotes();
void handle_custom_number_quotes_to_file();
int write_to_file(char *file_name, char *content);
#endif /**Quotes Header File*/
-
handle_single_quote.c
: Due to the flow of our program, we have an option where the user may select to see just one quote. So we're creating a function to handle that scenario.
This is the heart of the program, here, we're using functions and types from the curl header file (from installing libcurl) to perform the necessary actions.
#include "quotes.h"
/**
* handle_single_quote - handles the request and returns the data string
* Return - a String of the data retrieved and parsed
*/
void handle_single_quote()
{
CURL *curl;
CURLcode response;
char *sendOUtData;
string data_struct;
data_struct.data = malloc(1);
data_struct.len = 0;
// Initializ curl on the variable
curl = curl_easy_init();
if (curl)
{
curl_easy_setopt(curl, CURLOPT_URL, "https://api.quotable.io/random");
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &data_struct);
response = curl_easy_perform(curl);
sendOUtData = parse_result(data_struct.data);
system("clear");
printf("\n***********************************");
printf("\n%s\n", sendOUtData);
printf("***********************************\n");
free(sendOUtData);
sendOUtData = NULL;
curl_easy_cleanup(curl);
return;
}
printf("\n\nSorry, an error occured");
return;
}
Here's a breakdown of what's happening in the code above. This same set up applies to making any kind of get request from your C program.
-
CURL *curl;
: First, we declare a variable for our curl request using the CURL type. You'll initialize this variable to serve as the handler for your requests. -
CURLcode response;
: When we perform a request, this variable will hold the status of that request. For proper error handling, you may want to test:
if (response != CURLE_OK){
// Means something went wrong so you handle it
}
We didn't do that in our code snippet above, but you should know it.
char *sendOUtData;
: Here we're creating a char pointer to hold the string that will be returned after we parse the api response.Next we define a struct that we'll pass as an argument (a pointer to the variable) when setting up options for the curl request. This can be a char pointer if you want, provided you pass an its address to set option.
string data_struct;
data_struct.data = malloc(1);
data_struct.len = 0;
curl = curl_easy_init();
: Initialize the handler variable you defined earlier on. Note that this initialization will be used throughout that request cycle, so all options set on that handler will be the same. If you are doing this within a loop, and you want to change maybe the url, then you also have to initialize the handler within a loop and then be able to pass different setup options. If the initialization fails, it returns a NULL value, so you have to check if curl, before setting other options and performing the request.Setting the options for the request to the url:
curl_easy_setopt(curl, CURLOPT_URL, "https://api.quotable.io/random");
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &data_struct);
response = curl_easy_perform(curl);
By default, the curl handler will make a GET Request to the provided url. To make POST requests, kindly read up about it using man libcurl
.
Line 1: using the curl_easy_setopt(....)
function, you pass in the handler as the first argument, then CURLOPT_URL (These are predefined Macros from the curl header file), and the url you want to make a request to.
Line 2: Here we're passing a function pointer (not calling the function) alongside the MACRO CURLOPT_WRITEFUNCTION that marks our write_callback as the function to call to perform write operation of the response.
Line 3: For the next set option function, using the CURLOPT_WRITEDATA macro, we pass in a pointer to our struct defined earlier (remember I said this doesn't have to be a struct). It just needs to be a pointer to somewhere you want to store your string.
Line 4: Lastly, you call the curl_easy_perform(curl)
passing the curl handler you initialized earlier. This function will perform the request for you based on the options you have set up using curl_easy_setopt(....)
earlier. And then return a response code to the variable response
. At this point, you can print out the response which would now be stored in data_struct since that's where we passed a pointer to.
sendOUtData = parse_result(data_struct.data);
: This is useful for us because of what we're trying to achieve in our program. We have defined a function to parse the result and extract some strings from it. You'll see the definition below.curl_easy_cleanup(curl);
: This last line is very important. You call this function and pass in your curl handler to help you clean up all allocations that was done in processing your request. If you might also free any memory you allocated (for example sendOutData which will be gotten from the allocation made within parse_result(...)).parse_result.c
: I've documented the code so that it's much easier for you to follow and see what's going on. Just read through it and you'll understand.
#include "quotes.h"
/**
* parse the result to be pretty
* @result: JSON Looking result
* Return - Prettified string
*/
char *parse_result(char *result)
{
// using tyese types from the jansson header file, create a variable
// to hold the error (if any) when parsing your json string
json_error_t error;
// The root will hold the processed string when we call json_loads and pass in our char pointer (result parameter)
json_t *root, *content, *author;
// Create variables that will hold the key:value from the parsed json
// and other variables we might need
char *content_str, *author_str, *cleaned_result, *pad_string = "Quote by: ";
// call json loads, pass in the json string from result, and a pointer
// to the error variable declared earlier
// If an error occurs, root will hold a NULL value so we can print the error and exit the program
root = json_loads(result, 0, &error);
if (!root)
{
fprintf(stderr, "error: on line %d: %s\n", error.line, error.text);
printf("Try again\n");
exit(EXIT_FAILURE);
}
// Now you can retreive the key value of the 'variable' in the json string
// you want to retrieve. In our case, the quotable api returns the quote itself
// as content and the author as autho
content = json_object_get(root, "content");
author = json_object_get(root, "author");
// If an error occurs, we handle it and return NULL back
if (!json_is_string(content) || !json_is_string(author))
{
fprintf(stderr, "error: content or author is not a string\n");
json_decref(root);
return NULL;
}
// Here we pick out the value from the key:value string retreived
// earlier in content and author variable above
content_str = (char *)json_string_value(content);
author_str = (char *)json_string_value(author);
// We're allocating memory for a variable to hold the string the way we want it
// This is not necessary. you can already print out the content_str
// and author_str if that's what you want
cleaned_result = malloc(strlen(content_str) + strlen(author_str) + 4 + strlen(pad_string));
if (!cleaned_result)
{
fprintf(stderr, "error: could not allocate memory for cleaned_result\n");
printf("Try again\n");
json_decref(root);
return NULL;
}
// Construct the cleaned result
sprintf(cleaned_result, "%s\n\n%s%s", content_str, pad_string, author_str);
// Clean up json handler. This is important to help free any used memory
json_decref(root);
// Here we are returning the cleaned result to the place where
// the function was called. Since it was allocated for with malloc
// we also have to free it after using where it returned to.
return cleaned_result;
}
Conclusion
With the code snippets you've seen so far, you should already have a good idea of how the program works, and how you can use this guide to set up making network request from your C programs.
When compiling your code, make sure to link the two libraries (if you're using gcc):
gcc *.c -l curl -l jansson -o quotes
You can access the source code on github so see how the other functionalities of the program are coded.
SOURCE CODE HERE
To learn more about how you can make POST Requests, send data, or set headers, you can use the online man pages (CLICK HERE)
Also, make sure to handle errors, with more robust error handling when writing C programs as well as free any allocated memories to prevent memory leaks. (Though, there seems to be some memory leak issues with the libcurl library).
Cheers and happy coding!
Top comments (1)
I hope you enjoy it, and maybe have a little laugh! Happy Coding.