: 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 three options:

  1. You can use the static JvmLoader.SetConfigFile() method before you call GetJvmLoader(), or
  2. You can use the JvmLoader.GetJvmLoader() factory method that takes a filename as its first argument, or
  3. You can use IJvmLoader.Read() one or more times after you have created a JvmLoader. This will aggregate the settings in the referenced configuration files into the configured effective settings.

The first method is particularly useful if you want to leave the creation of JvmLoader 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.

Process p = Process.GetCurrentProcess();
string dir = new FileInfo(p.MainModule.FileName).DirectoryName;
string config = dir + @"\config.xml";

if( new FileInfo(config).Exists ) {
    IJvmLoader loader = JvmLoader.GetJvmLoader( config );
    ...
    // optionally make configuration API calls that override/check the config file
    loader.JvmPath = ... ;
    loader.DashDOption[ ... ] = ...;
}
else {
    // log the error and quit
    ...
}

...

try {
    IJvm jvm = loader.Load();
}
catch( System.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().

Example

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