Programming Topics + CVu Journal Vol 27, #5 - November 2015
Browse in : All > Topics > Programming
All > Journals > CVu > 275
Any of these categories - All of these categories

Note: when you create a new publication type, the articles module will automatically use the templates user-display-[publicationtype].xt and user-summary-[publicationtype].xt. If those templates do not exist when you try to preview or display a new article, you'll get this warning :-) Please place your own templates in themes/yourtheme/modules/articles . The templates will get the extension .xt there.

Title: Building C & C++ CLI Programs with the libCLImate Mini-framework

Author: Martin Moene

Date: 08 November 2015 08:55:43 +00:00 or Sun, 08 November 2015 08:55:43 +00:00

Summary: Matthew Wilson presents a framework for simplifying CLI programs.

Body: 

This article, the third in a series looking at software anatomy, builds on the material discussed in the first two instalments by discussing libCLImate, a mini-framework for command line interface (CLI) programming that encapsulates as much of the boilerplate as possible, and how it may be used in combination with program suite-specific libraries that encapsulate the rest, leaving the CLI application programmer to concentrate only on the interesting parts of the application development.

Introduction

In the first instalment of this series, ‘Anatomy of a CLI Program written in C’ [1], I considered in some depth the different aspects of CLI program construction, and expressed my desire to find a way to stop spending so much time thinking about and working on the fundamental aspects of program construction and focus instead on the interesting parts of the different problems programs need to solve. In the second instalment, ‘Anatomy of a CLI Program written in C++’ [2], I considered how to apply disparate utility libraries in the design and implementation of CLI programs, with the intention of separating out the boring boilerplate from the program-specific code, to promote flexibility, reuse, and testability.

In this, the third instalment, I discuss the reification of these previous intentions and considerations in the form of the libCLImate mini-framework, and its use in combination with program suite-specific libraries to drastically simplify the effort in creating CLI programs in C and C++.

libCLImate requirements

The requirements for the framework included:

In essence: make the job of the CLI programmer less boring, so they can do it faster and better.

Tour of the code

Because the libCLImate library comprises just a few small source files, it might be most illuminating to learn about the library by walking through the code.

Interface

There are six header files of interest to a user of the library (under the include directory):

libclimate/main.h

If you’re writing a C program you would probably include libclimate/main.h (an elided form of which is shown in Listing 1).

#include <libclimate/main/api.h>
int
main(
  int     argc
, char**  argv
)
{
  void* reserved = NULL;
  int (*pfn)(int, char**, void*);
#ifdef __cplusplus
  pfn = libCLImate_main_entry_point_Cpp;
#else /* ? __cplusplus */
  pfn = libCLImate_main_entry_point_C;
#endif /* __cplusplus */
  return (*pfn)(argc, argv, reserved);
}
			
Listing 1

As is strikingly obvious, including this file defines for you the main() entry point for the program, in terms of

both of which we’ll discuss shortly. Naturally, you can only include this file into one compilation unit of your program.

libclimate/main.hpp

If you’re writing a C++ program you would probably include libclimate/main.hpp:

<CodeListing> #include <libclimate/main.h></CodeListing> <CodeListing> #include <libclimate/main/api.hpp></CodeListing>

Note that I’ve said ‘probably’ in both cases. If you’re using a framework that itself provides main() – I recall that ACE [3] does that, and there are doubtless others – then you would instead eschew these two files and go to the underlying API files libclimate/main/api.h and libclimate/main/api.hpp.

libclimate/main/api.h

The first of these, libclimate/main/api.h, is the primary header file for the library. The abridged contents are shown in Listing 2.

#include <libclimate/common.h>
#include <libclimate/internal/clasp.clasp.h>
#include <stdio.h>

/* API globals */
/* The application-defined CLASP aliases array */
#ifdef __cplusplus
extern "C"
#else /* ? __cplusplus */
extern
#endif /* __cplusplus */
clasp_alias_t const libCLImate_aliases[];

/* API callbacks */
/* Application-defined program entry point 
   (for C) */
#ifdef __cplusplus
extern "C"
#else /* ? __cplusplus */
extern
#endif /* __cplusplus */
int
libCLImate_program_main_C(
  clasp_arguments_t const* args
);

/* Application-defined program entry point 
   (for C++) */
#ifdef __cplusplus
extern "C++"
int
libCLImate_program_main_Cpp(
  clasp_arguments_t const* args
);
# define libCLImate_program_main libCLImate_program_main_Cpp
#else /* ? __cplusplus */
# define libCLImate_program_main libCLImate_program_main_C
#endif /* __cplusplus */

/* API functions */
#ifdef __cplusplus
extern "C"
{
#endif

/* main */
#ifdef __cplusplus
int
libCLImate_main_entry_point_Cpp(
  int     argc
, char**  argv
, void*   reserved
);
#endif /* __cplusplus */

#ifndef __cplusplus
int
libCLImate_main_entry_point_C(
  int     argc
, char**  argv
, void*   reserved
);
#endif /* !__cplusplus */

/* exit */
void
libCLImate_exit_immediately(
  int   exitCode
, void (*pfn)(int exitCode, void* param)
, void* param
) /* noexcept */
;
#ifdef __cplusplus
extern "C++"
void
libCLImate_unwind_and_exit(
  int   exitCode
);
#endif /* __cplusplus */
/* usage */
int
libCLImate_show_usage(
  clasp_arguments_t const*  args
, clasp_alias_t const*      aliases
, FILE*                     stm
, int                       verMajor
, int                       verMinor
, int                       verRevision
, int                       buildNumber
, char const*               programName
, char const*               summary
, char const*               copyright
, char const*               description
, char const*               usage
, int                       showBlanksBetweenItems
);
int
libCLImate_show_usage_header(
  clasp_arguments_t const*  args
. . . // as libCLImate_show_usage()
);
int
libCLImate_show_usage_body(
  clasp_arguments_t const*  args
. . . // as libCLImate_show_usage()
);
int
libCLImate_show_version(
  clasp_arguments_t const*  args
, clasp_alias_t const*      aliases
, FILE*                     stm
, int                       verMajor
, int                       verMinor
, int                       verRevision
, int                       buildNumber
, char const*               programName
);

#ifdef __cplusplus
} /* extern "C" */
#endif
			
Listing 2

This breaks down as follows:

A bit grandiose, perhaps, but the libCLImate (mini-)framework can be considered a powerful and semi-intelligent ExecuteAroundMethod [4]. As such, the application programmer is required to provide the effective entry point and, so that command-line arguments can be processed on the application programmer’s behalf (by the CLASP library [5]), the CLASP aliases array. Hence, there are three application-defined constructs, two of which must be defined (depending on whether you're writing a C or C++ program). The aliases array is familiar from both previous instalments in this series ([1, 2]) and for libCLImate it must have the name libCLImate_aliases. The effective entry point must be called libCLImate_program_main_C() (for C) or libCLImate_program_main_Cpp() (for C++), as in Listing 3.

/* example.c */
#include <libclimate/main.h>
#include <stdlib.h>
clasp_alias_t const libCLImate_aliases[] =
{
  CLASP_ALIAS_ARRAY_TERMINATOR
};
int
libCLImate_program_main_C(
  clasp_arguments_t const* args
)
{
  return EXIT_SUCCESS;
}
			
Listing 3

There is a preprocessor macro libCLImate_program_main that resolves to libCLImate_program_main_Cpp or libCLImate_program_main_C as appropriate. You may wonder why not simply declare a single function of that name; this is explained later in this article.

The remainder of the contents of the main header file are API functions. The first pair are the framework entry points, as discussed earlier. If you are handling the definition of main() separate to libCLImate and so are not including libclimate/main.h or libclimate/main.hpp, then you will invoke one of these (libCLImate_main_entry_point_C() for C; libCLImate_main_entry_point_Cpp() for C++) once in your program’s execution.

The early exit functions are next, and are discussed separately shortly.

The usage helpers are thin wrappers over the CLASP [5] facilities that have been discussed in previous instalments [1, 2], so I won’t discuss them further here. Note, however, that they still involve many parameters – this will be addressed satisfactorily when I show how libCLImate may be used with CLI program suites.

libclimate/main/api.hpp

As you may have guessed from the earlier definition of libclimate/main.hpp, the contents of libclimate/main/api.hpp are defined largely in terms of libclimate/main/api.h:

<CodeListing> #include <libclimate/main.h></CodeListing> <CodeListing> #include <libclimate/main/api.hpp></CodeListing>

libclimate/implicit_link/core.h

This is a standard-fare implicit link header file, for use with those compiler suites (such as Visual C++) that support that technique, to link implicitly to the requisite (to the compilation conditions, e.g. release, multithreaded, multibyte-string, ...) libCLImate static library.

libclimate/implicit_link/common_implicit_link.i

Since libCLImate is implemented in terms of several other libraries – CLASP, Pantheios [6], recls [7] (Windows-only), this file includes some common, always-used libraries’ implicit-link headers (see Listing 4).

#include <libclimate/implicit_link/core.h>

#include <systemtools/clasp/implicit_link.h>

#include <pantheios/implicit_link/util.h>
#include <pantheios/implicit_link/core.h>

#ifdef _WIN32
# include <recls/implicit_link.h>
#endif
			
Listing 4

Pantheios aficionados will likely pick up that only the core and util libraries are specified, and why: front-end and back-end libraries are not specified, precisely because it is not the business of a general purpose mini-framework such as libCLImate to prescribe the specifics of the diagnostic logging control and output used by its users. Thus, if you’re using implicit-linking with libCLImate you will need to specify additionally which front/back-end libraries you require; specifying linking of core and util will not be necessary (but is harmless if you do).

Implementation

I’m not going to show much of the implementation, as a lot of it is simply a gathering together of notions previously espoused (in this forum), and you’re all welcome to simply browse it in (and fork it from!) the GitHub repo (http://github.com/synesissoftware/libCLImate).

There are five implementation files and one internal header file (in the src directory):

The first five of these are discussed in the next two sections. usage_etc.c does little more than provide the implementations for the helper functions mentioned earlier, including invoking the requisite CLASP API functions (using C Streams, aka FILE*) and determining the console width (if not piped).

Supporting C and C++

We all know that there can be only a single definition of main() in a link-unit. You are likely to know also that calling a C++ function that may throw exceptions from a C function yields undefined behaviour.

So, in order to support both C and C++ from the same library, we have to make sure that main() is not defined in the library. As you’ve already seen, main() is defined within the program’s object code by including libclimate/main.h(pp) in one compilation unit in the project. It is then implemented in terms of either:

These two files have the same logical structure:

However, the libraries used – Pantheios.Extras.*, CLASP – have very different behaviours depending on whether they are compiled in C or C++. Most importantly, in C++: Pantheios.Extras.Main performs outermost-scope exception handling, issuing contingent reports (to standard error stream) and diagnostic log statements; and CLASP handles command line-related exceptions along the lines of “MyProgram: unrecognised flag '--stranger'; use --help for usage”.

Thus, the separation is necessitated by the rules of the language as they pertain to main()’s uniqueness, by the need to support exceptions in order to provide rich handling of common non-normative conditions (such as a user specifying an unrecognised flag/option). That it also facilitates the ability of the user to provide his/her own main() and call into libCLImate explicitly may be thought a bonus.

Exceptions and early exit

The necessary handling of exceptions just mentioned also affords us the ability to take away another common (at least to me) but non-standard bit of repetitive work.

As you may know, gentle reader, when (std::)exit() is invoked, the implementation does not cause the destruction of any automatic variables. Hence, the output of Listing 5 is ‘in and out’ when run without arguments, but only ‘in’ when given one or more arguments.

#include <stdio.h>
#include <stdlib.h>
class inout
{
public:
  inout() { fputs("in", stdout); }
  ~inout() { fputs(" & out\n", stdout); }
};
int main(int argc, char* argv[])
{
  inout io;
  if(1 != argc)
  {
    exit(1);
  }
  return 0;
}
			
Listing 5

What this means for sophisticated CLI programs is that calling (std::)exit() can be a bad idea, because things won’t get cleaned up by the C++ runtime. For sure, many things, such as file handles, will be cleaned up by the operating system (and some things by the C runtime), but it may be unwise to rely on that, because none of it will be done with an understanding of what those objects were doing from a ‘C++ point of view’. (I know that’s a horribly woolly description, but I can’t think of a good example right now, and I trust, gentle readers, that you can go with me regardless...)

Anyway, we do want to be able to perform an early exit to the program, and we don’t (always) want to achieve this by N returns (where N can be a very large number). Well, in C++, there’s a well-known control-transfer mechanism [10]: exceptions. Why not throw an exception?

Two problems. First, which exception do we throw? There’s no standard exception to indicate a request to exit a program (or thread). I’ve written many such things over the years: some within general-purpose C++ libraries; others within program suites. The problem with the former is that it’s more coupling for something totally fundamental and uninteresting. The problem with the latter is that one ends up in copy-paste hell. Coupling or copying – yuck!

The second problem is more subtle, but much more significant. It is received wisdom (which I too espouse [11]) that all exception types should be derived from std::exception. But really this rubric is too simplistic. What I think it should actually mean is that all exceptions types whose instances we may interact with should be derived from std::exception. The reason is that one should only catch what is one’s business to catch. Or, put more powerfully, one should not catch what it is not one’s business to catch.

If we define our putative end-program-exception to derive (by whatever depth) from std::exception, then it is possible that application code (or library code that is at a higher level of abstraction and dependency than our end-program-exception) may intercept and quench it. Of course, any code that does such a thing is (overwhelmingly likely) in error, but practical experience (in C++, and in many other languages – C# being the standout worst) tells me that this will happen.

So, there’s a strong argument to be made to have an end-program-exception that is not part of the std::exception hierarchy. (Note: there are some arguments against, such as subversion of a std::exception-derivation assumption in the implementation of a C-API boundary, but I’m running out of space to discuss here. Pepper me with email on the accu-general mailing list if you wish.)

Going with this argument, however, means we risk exposing our exception to good practice – pun intended! – to the wider world, giving a bad example. That’s where quiet_program_termination_exceptioncomes in: it carries an exit code from the throw point, which is in libCLImate_unwind_and_exit() (in unwind_and_exit.cpp) to a handler in the ExecuteAroundMethod layered function stack in main_entry_point_Cpp.cpp. No user of libCLImate is exposed to this class, not even polymorphically.

Being practical, however, we must recognise that there may be some circumstances in which this isolation from std::exception is not desired. So, there are a bunch of pre-processor symbols that may be defined during the building of libCLImate that allow the exception to:

The name

I asked the friendly fellows at ACCU-general for assistance in naming the library. There were several apposite suggestions (along with the odd inevitable ‘wheel already invented’ carp), but one stood out. Jonathan Wakeley offered libCLImate, ostensibly because it’s a supporting (mate) library for CLI, but really so that Phil Nash could pun that ‘every pull request will result in CLI-mate change’. In the face of such wit I was powerless to resist. 

Program suites

Applying libCLImate results in a substantial reduction in size and simplification of the implementation of standalone CLI programs. However, there are still some aspects that involve too much work:

By combining libCLImate with a program suite-specific library, the implementation of each program in the suite can be distilled down so much as to achieve the primary expressed aim of the library: Require of the application programmer only a single entry-point function and declarative specification of command-line arguments. In the remainder of this instalment I will demonstrate how that’s been achieved for the Synesis Software Source Tools program suite via the proprietary library libSrcToolMain.

libSrcToolMain consists of three groups of source files:

We’re interested in the CLI framework files, of which there are nine worth exploring:

The files core.h and common_implicit_link.i serve the same functions as their libCLImate analogues. But because all programs in the program suite have common diagnostics, the latter file also includes implicit link inclusions for Pantheios’ fe.simple front-end and be.N multiplexing back-end (in conjunction with the console, file and WindowsDebugger concrete back-ends).

The file diagnostics.c contains the program suite-specific back-end customisations, a small part of which is shown in Listing 6.

/* application-defined callbacks */
PANTHEIOS_CALL(void)
pantheios_be_WindowsDebugger_getAppInit(
  int                             backEndId
, pan_be_WindowsDebugger_init_t*  init
) /* throw() */
{
  STLSOFT_SUPPRESS_UNUSED(backEndId);

  init->flags |=
    PANTHEIOS_BE_INIT_F_NO_PROCESS_ID;
  init->flags |= PANTHEIOS_BE_INIT_F_HIDE_DATE;
  init->flags |=
    PANTHEIOS_BE_INIT_F_HIGH_RESOLUTION;
}
. . .
			
Listing 6

The file program_identity_globals.h declares a bunch of global constants that collectively define the identity and version of the given program (see Listing 7). The definitions of these constants are provided in each program’s identity.cpp file (defined as described in [2]).

#include <SynesisSoftware/SourceTools/common.h>

/* application-defined globals */
#ifdef __cplusplus

extern "C"  const int         sourcetoolVerMajor;
extern "C"  const int         sourcetoolVerMinor;
extern "C"  const int         sourcetoolVerRevision;
extern "C"  const int         sourcetoolBuildNumber;

extern "C"  char const* const sourcetoolToolName;
extern "C"  char const* const sourcetoolSummary;
extern "C"  char const* 
  const sourcetoolCopyright;
extern "C"  char const* const
  sourcetoolDescription;
extern "C"  char const* const sourcetoolUsage;

#else /* ? __cplusplus */
extern      const int         sourcetoolVerMajor;
. . .

#endif /* __cplusplus */
			
Listing 7

The file standard_argument_helpers.h declares a number of helper functions that are useful when processing the command-line (Listing 8).

int SS_SrcTools_show_usage(
  clasp_arguments_t const*  args
, clasp_alias_t const*      aliases
, FILE*                     stm
);
int SS_SrcTools_show_version(
  clasp_arguments_t const*  args
, clasp_alias_t const*      aliases
, FILE*                     stm
);
void SS_SrcTools_display_usage_and_ \
    rewind_if_requested(
  clasp_arguments_t const*  args
, char const*               flagLongForm
, FILE*                     stm
);
void SS_SrcTools_display_version_and_ \
    rewind_if_requested(
  clasp_arguments_t const*  args
, char const*               flagLongForm
, FILE*                     stm
);
			
Listing 8

As can be seen plainly, these functions do not have a list of parameters as long as your arm: that’s because each one is defined (in show_usage.c and standard_argument_helpers.cpp) in terms of the requisite libCLImate API functions passing the identity globals as appropriate. This leads to much more transparent (and maintainable) application code.

But the real gain is in the two files not yet mentioned: main.cpp and tool_main_outer.cpp. main.cpp (Listing 9 includes libclimate/main.hpp and defines the (libCLImate-perspective) effective entry point libCLImate_program_main() in terms of the declared function tool_main_outer(), which it calls after first checking for, and reacting to, the --help and --version UNIX-standard flags.

#include <SynesisSoftware/SourceTools/
  program_identity_globals.h>
#include <SynesisSoftware/SourceTools/
  standard_argument_helpers.h>
#include <libclimate/main.hpp>

extern "C++"
int
tool_main_outer(
  clasp::arguments_t const* args
);
int
libCLImate_program_main(
  clasp::arguments_t const* args
)
{
  namespace ssst = SynesisSoftware::SourceTools;

  /* process standard flags */
  ssst::display_usage_and_unwind_if_requested(
    args, "--help", stdout);
  ssst::display_version_and_unwind_if_requested(
    args, "--version", stdout);

  /* process command-line */
  return tool_main_outer(args);
}
			
Listing 9

The function tool_main_outer() is defined in tool_main_outer.cpp (Listing 10) in terms of the declared (libSrcToolMain-perspective) effective entry point tool_main_inner() which it invokes within a try-catch whose catch clauses handle program suite-specific exceptions in a suite-common manner.

extern "C++"
int
tool_main_inner(
  clasp::arguments_t const* args
);

int
tool_main_outer(
  clasp::arguments_t const* args
)
{
  try
  {
    return tool_main_inner(args);
  }
  catch(<ps-specific-exc1>& x)
  {
    . . .
  }
  . . .
}
			
Listing 10

While that might seem a lot of code to present in an article, it’s pretty small in terms of a library, and it allows all programs within the program suite to substantially reduce the amount of boilerplate, such that each program’s entry.cpp (defined as described in [2]) consists solely of the libCLImate_aliases array definition and the effective program entry point tool_main_inner(), as shown in Listing 11 (extracted from the fsrtax source tool’s entry.cpp). The result is maximised application-specific code : boilerplate ratio in each program’s entry.cpp; all the program suite-specific boilerplate is in the program suite library, and all the CLI boilerplate is in libCLImate.

#include "fsrtax.hpp"
#include "identity.h"
#include <SynesisSoftware/SourceTools/program_identity_globals.h>
. . .
extern "C"
clasp::alias_t const libCLImate_aliases[] =
{
  // standard flags
  SS_SRCTOOLS_STD_FLAG_help(),
  SS_SRCTOOLS_STD_FLAG_version(),

  // program logic
  CLASP_BIT_FLAG("-D", "--details",
    FSRTAX_F_SHOW_DETAILS,
    "displays all details"),
  . . .
  CLASP_ALIAS_ARRAY_TERMINATOR
};
extern "C++"
int
tool_main_inner(
  clasp::arguments_t const* args
)
{
  int flags = clasp::check_all_flags(args,
    libCLImate_aliases);
  . . .
  clasp::verify_all_options_used(args);
  . . .
  return process(flags, . . .);
}
			
Listing 11

Summary

For all that frameworks are loathsome things, restricting freedom and constraining choice, they are often a necessary evil in programming, because sometimes the advantages outweigh the disadvantages. I believe libCLImate represents just such a net-positive balance, and have been using it for some time now to focus mostly on the interesting parts of my CLI projects.

Next time

In the next instalment (which may be a while because I’m starting a new job next week!), I want to move away from C and C++ for a bit, perhaps to something a bit more modern such as Go, or maybe flesh out the structure I’ve adapted for writing Ruby CLI programs of late.

Acknowledgements

Thanks to Garth Lancaster for review pointers, and to Steve Love for editorial patience.

References

[1] Anatomy of a CLI Program written in C, Matthew Wilson, CVu September 2012.

[2] Anatomy of a CLI Program written in C++, Matthew Wilson, CVu September 2015.

[3] https://en.wikipedia.org/wiki/Adaptive_Communication_Environment

[4] http://c2.com/cgi/wiki?ExecuteAroundMethod

[5] An Introduction to CLASP, part 1: C, Matthew Wilson, CVu, January 2012

[6] http://pantheios.org/; http://github.com/synesissoftware/Pantheios

[7] http://recls.org/; http://github.com/synesissoftware/recls

[8] http://pantheios.org/; http://github.com/synesissoftware/Pantheios.Extras.Main

[9] http://pantheios.org/; http://github.com/synesissoftware/Pantheios.Extras.DiagUtil

[10] Quality Matters 5: Exceptions: The Worst Form of Exception Handling, Apart From All the Others, Matthew Wilson, Overload, #98, August 2010

[11] Quality Matters 6: Exceptions For Practically-Unrecoverable Conditions, Matthew Wilson, Overload, #99, October 2010

[12] http://stlsoft.org/; http://github.com/synesissoftware/STLSoft-1.9

Notes: 

More fields may be available via dynamicdata ..