: config tutorial (v3)

Lesson 3: File-based Configuration

Introduction

File-based configuration is a popular option for applications that might have to be run in more than one mode. Our configuration files are XML-based and support multiple sections of which only one is selected to be the active one. Consider the following scenario:

Your mixed-language application is happily deployed with a hard-coded Java configuration that works. Then a user reports a crash that leaves a Java stacktrace behind. You want to run the application with the -Xcheck:jni option to make sure that all JNI calls are done properly (you don't do that for production use because it has a performance impact) and you want to debug into the Java parts of the application. But for that you need to set several JVM options to allow your Java debugger to attach to the JVM that's been loaded into your native process. How can you do that?

With a file-based configuration that's really easy. You simply put two configuration sections into your configuration file, for example named "Production" and "Debug". Under normal circumstances, the application runs with the "Production" settings but when you need to do some debugging you simply switch to the "Debug" configuration that contains your debugger settings.

The configuration file elements are a bit awkward because we were constrained by elements supported by the early .NET configuration files (we wanted the configuration files to be usable by JuggerNET and JunC++ion applications.) Nevertheless, the files are easy to understand. Here's an example from the file-based configuration example bundled with the code generator:

<Codemesh>
    <Runtime>

        <!-- you can have many different configurations in your config file;
             They just have to end with the string ".JvmSettings", otherwise they
             can have any valid XML name you choose.  This element specifies
             which one you wish to use by the name prefix.
            -->
        <Loader name="Production"/>

        <!-- a JVM configuration options for production use;
             there are many more, but most can be specified via corresponding
             -D or -X settings
            -->
        <Production.JvmSettings>
            <add key="Type" value="Codemesh.JuggerNET.SunJava2JvmLoader" />
            <add key="ClassPath" value="../.." />
            <add key="InitialHeapSize" value="64" />
            <add key="CheckJNI" value="false" />
            <add key="TraceLevel" value="TraceWarnings" />
        </Production.JvmSettings>

        <!-- the -D options (system properties) for Production use -->
        <Production.Options>
            <add key="FOO" value="!!! foo !!!" />
            <add key="BAR" value="!!! bar !!!" />
            <add key="BAZ" value="!!! baz !!!" />
        </Production.Options>

        <!-- the -X options for Production use -->
        <Production.XOptions>
            <add key="mx" value="256m" />
        </Production.XOptions>

        <!-- a JVM configuration for debug use -->
        <Debug.JvmSettings>
            <add key="Type" value="Codemesh.JuggerNET.SunJava2JvmLoader" />
            <add key="ClassPath" value="../.." />
            <add key="InitialHeapSize" value="16" />
            <add key="CheckJNI" value="true" />
            <add key="TraceLevel" value="TraceInfo" />
        </Debug.JvmSettings>

        <!-- the -D options (system properties) for Debug use -->
        <Debug.Options>
            <add key="FOO" value="!!! foo-debug !!!" />
            <add key="BAR" value="!!! bar-debug !!!" />
            <add key="BAZ" value="!!! baz-debug !!!" />
        </Debug.Options>

        <!-- the -X options for Debug use -->
        <Debug.XOptions>
            <add key="mx" value="16m" />
        </Debug.XOptions>

    </Runtime>
</Codemesh>

To use this file to configure the application, you have two options:

  1. You can use the static xmog_jvm_loader::setConfigFile() method, or
  2. You can use the xmog_jvm_loader::get_jvm_loader() factory method that takes a filename as its first argument.

The first method is particularly useful if you want to leave the creation of xmog_jvm_loader instance to the application while setting the configuration file in a configuration hook inside a library.

Often you will wish to use a configuration file that is co-located with your binary executable. A common pattern for such use cases is to use an environment variable to define the application's home directory and then derive all paths, including the configuration file path, from that directory. The following code snippet illustrates this pattern.

// the environment variable would either be set by a launcher script
// or be configured globally by the application installer
const char * homeDir = getenv("MYAPP_HOME");
if( homeDir == NULL )
    homeDir = ".";

std::string strHomeDir( homeDir );
std::string strConfigFile( homeDir + "/bin/myconfig.xml" );
xmog::util::file fConfig( strConfigFile );

if( fConfig.exists() ) {
    xmog_jvm_loader & loader = xmog_jvm_loader::get_jvm_loader( strConfigFile.c_str() );
    ...
    // optionally make configuration API calls that override/check the config file
    loader.setJvmPath( ... );
    loader.setDashDOption( ... );}
else {
    // log the error and quit
    ...
}

...

try {
    xmog_jvm * pJvm = loader.load();
}
catch( xmog_exception & e ) {
    ...
}

We repeat the following section from the explicit lesson because it is important and applies to all configuration approaches.

When to Configure

As you can see in the above code snippet, we explicitly try to load the JVM after the configuration is complete. While this is not strictly necessary (the JMS Courier runtime supports on-demand loading when Java is required) it is highly recommended. Doing it this way gives you one and only one point at which the JVM initialization could fail, making the debugging of configuration issues much easier.

You want to make sure that the configuration is complete before you make your first Java call or the JMS Courier runtime will simply use its default settings to load a JVM when asked to do something in Java. Most JVM settings are "frozen" in place once the JVM has been loaded and cannot be modified after the fact. Therefore, particularly if you have a large codebase, it is important to perform the configuration as early as possible. We recommend that you do it right at the beginning of your program's main().

To the novice C++ developer it might seem that this will guarantee that the runtime configuration has happened when the first call to Java needs to be made. Unfortunately, that is not always the case. C++ has to perform a host of initialization tasks before it can call main(). For example, the constructors of all globally declared objects have to be called prior to calling main() or your program code might be using uninitialized objects. Thankfully, the C++ infrastructure takes care of this for you. It is exactly this invisible initialization that can cause problems though.

Do not declare a global proxy type instance somewhere in your code and initialize it in such a way that Java code has to be executed, for example by initializating a proxy java::lang::String with a string literal:

#include "java_lang_pkg.h"

// DO NOT DO THIS
// a global String instace we use over and over
static const java::lang::String   SECRET = "shared secret";

Because the SECRET object requires that the JVM be loaded in order to convert the string literal to a Java String instance, the JMS Courier runtime will have no choice but to load a JVM during the String instance's constructor call. Crucially, this happens before your configuration code in main() is executed. It is safe to initialize global proxy instances to null or to use their default constructor, which does not result in a Java call.

Example

You can find an example in the code generator distribution at examples/cpp/v3/configuration/filebased. The readme.html file in that directory contains a description and the instructions on how to build and run the example.