Core API

There are a handful of “core” library functions that can be used anywhere.

GUID

The GUID class provides v4 GUIDs. GUIDs are stored internally as an array of 16 bytes. These can be parsed from standard GUID strings or supplied as an array of bytes.

A few methods that might be interesting:

  • The default constructor - A null (zero’d) GUID.
  • generate() - A static member of GUID, generates new GUIDs.
  • str() - Returns a string representation of a GUID.
  • data() - Returns the raw 16-byte array of the GUID.

Byte Buffer

You can think of a byte buffer as a slightly more efficient way to ship bytes around then std::vector<uint8_t>.

A byte buffer is particularly nice for smaller buffers, as the memory it uses is first allocated off the stack.

Further, there are provisions for wrapping existing arrays of data (via a constructor), so you can easily wrap byte arrays and deserialize them without copying data.

The byte buffer uses a copy-on-write scheme, making copies of byte buffers very cheap. If you attempt to write to a buffer that is using copy-on-write, it will force a full deep copy at that point in time.

Copying and moving byte buffers typically incur a <10ns penalty. Modifying a copied buffer will incur a cost of a memcpy.

Argument Parsing

Ark provides a standard GNU-style argument parser, which all of its utilities make use of.

As a simple example:

#include "ark/core/argument_parser.hh"

core::ArgumentParser parser;

parser.add_option("output")
      .add_name("o")
      .required()
      .requires_argument()
      .help("The destination output directory.");

parser.add_positional("files")
      .required()
      .consumes_remaining_arguments()
      .help("The files you wish to copy.");

parser.set_description("Copies files to an output directory.");

// Parse the arguments here -- by default, the parser will exit if
// an error occurs (bad arguments, or missing required arguments).
auto result = parser.parse(argc, argv);

// Now you can retrieve the arguments as strings... note the
// difference between "argument" (which returns the first or only
// only argument) and "arguments" (which returns all of them).
auto output = result.argument("output");
auto files = result.arguments("files");

Standard flags are also possible:

core::ArgumentParser parser;

parser.add_option("verbose")
      .add_name("v")
      .help("Enables verbose output.");

auto result = parser.parse(argc, argv);

// The -v option is now optional, and you can check if it was set
// with:
if (result.count("v") > 0)
{
    ...
}

You can allow options to occur more then once with the allow_multiple_uses flag.

Options can have as many names as you like, and they can be either short (single character) or long. Short names are set with a single dash (-o) and long names use a double dash (--output).

Options can consume either zero or one additional argument, but no more. If you want an option to consume many arguments, you need to specify it multiple times (allow multiple uses).

Positionals take no flag, and simple are called when something is passed in on the command line without an option. They can consume either one argument, or all of the remaining arguments until the end (or another option is hit).

For example, using the above copy parser:

./my_program file1.txt file2.txt file3.txt -o ~/output

Would result in having an “output” option of “~/output”, and a “files” option containing “file1.txt”, “file2.txt”, and “file3.txt”.

Finally, if you wish to mark that exactly one option in a group of options must be specified, you can use the require_mutually_exclusive_options API, like so:

core::ArgumentParser parser;

parser.add_option("load");
parser.add_option("save");

parser.require_mutually_exclusive_options({"load", "save"});

The caller must specify either --load or --save (but not both), or the arguments will fail to parse.

URL

This is a basic wrapper around URLs, allowing you to decode a URL into its various components, or to replace those components without trying to do string manipulation yourself.

For a simple example:

#include "ark/core/url.hh"

core::Url url("http://localhost:8080/index.html?log=25");

// Make the URL "http://localhost:8080/api/logs?log=25"
url.set_path("/api/logs");

// Make the URL "https://localhost/api/logs?log=25"
url.set_scheme("https");
url.set_port(0);

You can use the str() routine to get back the string-representation of the URL.

Filesystem

There are a few helper APIs for reading content from a file safely:

  • write_string_to_file
  • read_string_from_file

Both of these read or write an entire batch of content from or to a file. If any error occurs, they will throw.

These are both simpler and safer-by-default then using standard streams.

The write_string_to_file API takes a few additional optional options that allows you to have more control over how the file is written out:

  • Synchronize - force a fsync when writing a file
  • Atomic - write content to a backup file and rename over the target file

These flags can be used to help make a best-effort to ensure the data you have written to disk is on disk by the time it returns. These flags are a bitmask – consider setting both for the highest safety guarantees.

Backtraces

You can install signal handlers for your application, such that if you crash, you will get a human-readable backtrace:

#include "ark/core/backtrace.hh"

core::install_failure_signal_handlers();

Place that code at the top of your main() function, and if you crash (or have an unhandled exception) anywhere in your program, you will get a stack trace.

Two additional functions exist to make grabbing backtraces efficient:

  • capture_backtrace - Captures the stack trace, in program counter form, at your current location
  • resolve_symbol_at_program_counter - Use this to resolve the counters from capture_backtrace to names

Finally, if you just want to print the backtrace easily, you can use the print_backtrace function at any location to print the current stack.

Scope Exit

This class allows you to define a block of code (or lambda) that will execute when the current scope exits.

For example:

#include "ark/core/scope_exit.hh"

{
    core::ScopeExit guard([]() { std::cout << "Executing!" << std::endl; });
    // ... Some code
}

Once the scope exits (ie, code is done executing within the outer braces), you will see an ‘Executing’ message on your screen.

You can force a scope exit to “complete” (ie, execute) early by invoking its complete() method. This will cause it to execute its lambda function, and that function will not be executed when the scope exits.

Circular Buffer

A circular buffer allocates all of its memory upfront, and can be used like a typical STL container otherwise. We typically use these to avoid memory allocations once a program has started.

For example:

#include "ark/core/circular_buffer.hh"

CircularBuffer<uint32> buffer(10);

buffer.push(5);
buffer.push(6);

// Invoking buffer.pop() will now result in '5'.

Process Name

There are two helper APIs for retrieving the process/executable name, depending on your use case:

  • ark::core::get_this_process_invocation_name() - returns the equivalent of whatever is typically stored in argv[0] (for example, if you launched your binary with ./build/my-program, this will return ./build/my-program).
  • ark::core::get_this_process_name() - this gets just the base name of the launched executable (for example, if you launched your binary with ./build/my-program, this will return my-program).

These can make it possible to identify the name of the binary that is currently running, or possibly to resolve the path the binary is in (to find other paths, such as configuration files).