Green feathers falling down

Printing a file with the Apache Portable Runtime

31 July 2022

In my last post, I wrote about building an Apache module written in C.

In researching how to do this, I learnt about a the Apache Portable Runtime.

What's that?

It's a number of libraries and header files provided by the Apache Software Foundation which abstract away operating system operations, and allow your C code to become more portable. The Apache Portable Runtime (APR) was originally part of the Apache Server codebase. It got separated out, because having a way to write C code in a single codebase that you know you can compile for multiple platforms is a pretty nice thing to have!

Portability #

C was meant to be a portable language when it was created.

Unfortunately, it was never "write once, run anywhere", as operating systems offer different features and APIs, compilers act completely differently, and C simply grew up in the Wild West days of computing.

Some examples of the issues APR smooths over:

Pools #

The APR also provides an API to make memory management easier in C.

It uses a system of pools, where every piece of dynamically created memory gets assigned to a pool, which will remember what you've created. Then when you're finished with the pool, you tell it you're done by calling apr_pool_destroy.

After you call this, everything is freed! You don't have to go through all the pieces of data you've dynamically allocated to make sure that they've all been freed and the space has been given back to the operating system. The pool does that for you.

Getting set up #

Although I've just been raving about the portability of APR, these instructions are going to be based on compiling on and for 64 bit Windows, so if you're on Mac or Linux, some of these steps won't apply for you.

So how do we write a program which using the APR? First we're going need a compiler. If you're on Windows I've got some instructions on where to go to get the command line Visual Studio Compiler for C.

Next we're going to need the APR library and header files.

If you have Apache Server installed - you should already have them. You'll find the header files in the include folder and the library files in the lib folder. Both live in the root folder of apache.

If you don't already have Apache downloaded, you can either download the APR source and compile it (if you want to go through that). Or you can grab Apache for Windows from Apache Lounge.

Now open your favourite text editor. Mine is VSCode.

Boiler plate #

First we're going to need to include the APR libraries, and also we're going to grab the classic standard IO library:

#include <apr.h>
#include <apr_pools.h>
#include <apr_file_io.h>
#include <stdio.h>

These will pull in all the APR functions from these header files.

Now we'll write our main function:

int main(){

}

We start by initialising APR and creating our pool.

int main(){
  apr_initialize();
  apr_pool_t *pool;
  apr_status_t status = apr_pool_create(&pool, NULL);
  if(status != APR_SUCCESS){
        printf("Could not create pool");
  }
}

You'll find that usually C frameworks won't be set up for you; you'll have to call a function to start things up. There's no free lunch with a low level programming language. Here we need to call apr_initialize before we're allowed to do anything with APR functions.

Next we declare a pointer to a pool, then pass it to apr_pool_create. This initializes the pool for us. The second argument of apr_pool_create is the parent pool (APR gives you the ability to hold a hierarchy of pools where a parent pool can clean up its children). In our case, this is our first and only pool, so we pass NULL as the second argument.

Finally, it's always good practice to check if things have gone wrong. C doesn't have nice ways to do this like more popular programming languages today. Instead, APR functions will return an enum to represent their status. If things went well, we should see APR_SUCCESS.

Opening a file #

Now let's set up a file descriptor. This is an APR structure which will be able to use to read a file.

  apr_file_t *file;
  char *fname = "C:/path/to/a/random/file.txt";
  status = apr_file_open(&file, fname, APR_READ, APR_OS_DEFAULT, pool);
    if (status != APR_SUCCESS)
    {
        printf("Could not open file");
    }

First we declare a pointer to the file descriptor, then declare our file path as a pointer to the first char in the path, calling it fname. We use the & "Address of Operator" when passing the file descriptor to apr_file_open. apr_file_open's first argument isn't a pointer to a file descriptor. Instead it's a pointer to a pointer to a file descriptor. I don't know why it isn't just a pointer to the file descriptor, but there you go. The second argument is our file name. The third argument is the mode we want to open the file with.

Setting up a buffer #

Now that we've connected with our file, we still need to do some set up before we can read it into a string.

We need to allocate the required memory for where our string is going to be held. This is because we don't know the size of the file and so have to do this dynamically. So our first step is to find out how big the file is, then we can set up the space in memory (a memory buffer) to store the string of the file's contents.

Here's how we do this:

  apr_off_t offset = 0 ;

  apr_file_seek(file, APR_END, &offset);

  apr_size_t size = offset;

  char *buffer = apr_pcalloc(pool, size +1);

  offset = 0;

  apr_file_seek(file, APR_SET, &offset);

What's going on here? apr_file_seek is a function with various uses, but its main use is to move the cursor to a certain place in a file (the cursor is a number representing the current char we're reading from the file).

We want to find out how large the file is, so we move the cursor to the end of the file with the first call to apr_file_seek. The third argument is a pointer to an offset, which will change the cursor to a certain location by its value, but can also be used (as in this case) to get the location of the cursor after the function call. The function sets it to the current location of the cursor after it has changed position. Lastly, the second argument lets us tell it to move to the end of the file using the enum APR_END. After this offset will hold the length of the file in bytes (as a single char is a byte).

Now we create a size variable of type apr_size_t which is a long number that will represent the size of the file. We allocate our memory to hold our string in by setting a pointer to a char variable to the return value of a new APR function: apr_pcalloc.

This function allocates all the memory we need, and sets it all to zero. This is good because C strings need to end with a null character (which is the same value as zero), so the language knows when the string ends. This is also the reason we add one to the size, as we need that extra null character at the end. The first argument is which pool we want the memory to be under.

If we read the file now, the cursor would still be right at the end and we wouldn't get anything back from reading it. For this reason, we need to set our offset to zero, then use the same function from earlier, but this time use the APR_SET enum, which sets the cursor to the offset we provide (which is zero). I personally would probably prefer a different function that would just set the cursor to zero, but hey, it's C. What are you gonna do?

Reading the file #

Now it's time to read our string into the buffer, and print it to standard out!

  status = apr_file_read(file, buffer, &size);
  if (status != APR_SUCCESS)
  {
      printf("Could not read file");
  }
  else
  {
      printf(buffer);
  }

It's as easy as that! We pass apr_file_read the pointer to our file, the pointer to our buffer, and the pointer to our size (this one we hadn't previously stored as a pointer so need to dereference it with &). APR will copy the contents of the file into the buffer, then we're ready to print it out.

Cleaning up #

After we're finished, we need to close our file and terminate APR:

 apr_file_close(file);
 apr_terminate();
 return 0;

The entire source #

Here's what the source should look like, with all the checks and balances:

#include <apr_general.h>
#include <apr_pools.h>
#include <apr_file_io.h>
#include <stdio.h>

int main(){
  apr_initialize();
  apr_pool_t *pool;
  apr_status_t status = apr_pool_create(&pool, NULL);

  if(status != APR_SUCCESS){
        printf("Could not create pool");
  } 
  else { 
    apr_file_t *file;

    char *fname = "C:/path/to/a/random/file.txt";;

    status = apr_file_open(&file, fname, APR_READ, APR_OS_DEFAULT, pool);
    if (status != APR_SUCCESS)
    {
        printf("Could not open file");
    }
    else {
      apr_off_t offset = 0 ;
	
      apr_file_seek(file, APR_END, &offset);
    
      apr_size_t size = offset;
      char *buffer = apr_pcalloc(pool, size +1);
    
      offset = 0;
    
      apr_file_seek(file, APR_SET, &offset);

      status = apr_file_read(file, buffer, &size);
      if (status != APR_SUCCESS)
      {
          printf("Could not read file");
      }
      else
      {
          printf(buffer);
      }
      apr_file_close(file);
    } 
   }
   apr_terminate();
   return 0;
}

Compiling the program #

Now save the file as aprprint.c.

Then you can compile the program on x64 Native Tools Command Prompt by navigating to your source directory then typing the following command (replacing the paths with the paths to your Apache folder):

cl /nologo /MD /W3 /O2 /D WIN32 /D WINDOWS /D NDEBUG -I"C:\path\to\apache\include" aprprint.c kernel32.lib ws2_32.lib "C:\path\to\apache\lib\libaprutil-1.lib" "C:\path\to\apache\lib\libapr-1.lib" /nologo

Now you should get back silence from the compiler. Have a look at the error messages if your don't.

The last thing to do is to add the bin folder in apache to your environment variables path. This will mean the DLL's that Apache provides will be in scope for your program.

After all this, you can run the program with:

aprprint.exe

You're an APR programmer now! 😈

More! #

Try adding the file name as a command line argument to print off different functions. Or you could combine this program with my last post about Apache modules to make a static file server.

Here's a tutorial from Inoue Seiichiro about the APR which goes more in depth than I can.

Back to blog