cl

David Grace

Bay Area, California

Programmer, SysAdmin, Artist

Stupid Flex Compiler Tricks: Conditional Compiling For Fun And Profit

While I was working on several projects utilizing Adobe’s Flex SDK for Flash, I found myself growing wistful for the days of C-style macros and defines. In other words, a pre-processor. Why?

Let’s find out!

Every project usually hits a point where your build diverges as you factor in different levels of support or options. In other words, you want multiple builds for different runtime options. For example, let’s consider the simplest option of having two builds: a debug and a release version. The debug version could be anything from your normal program plus a smattering of simple trace log checkpoint to an entire debug sub-system which handles various testing scenarios. While it isn’t harmless to leave such things lying around in a final build, there are times when you might want to strip them out. You don’t want this code in the release version because it would allow cheating (since I am talking about game development) or you are trying to cut down the overall size of your binaries by excluding unused features.

Let’s wrap it up with an even more concrete example: My last game had an extensive debugging GUI that I wrote for testing level changes.

Debug GUI for Legend of Kalevala

The debug was used extensively in the final stages of testing and I wouldn’t have been able to finish the game without it. But I certainly didn’t want to include this in the final version of the released game.

As useful as the debug UI was, it could be used to cheat at online high scores. It also added an additional half a meg to the final size of Legend of Kalevala’s SWF, so I also wanted to trim that out. While I could have done this:

1
2
// Comment this out in the release build!
debug = new DebugGUI ;

That method might be fine for simple projects, but when you expand in scale it quickly becomes unmanageable and error-prone. But what if I could create a define called “DEBUG” and then check to see if that define is active. If so, include the “new DebugGUI” call, otherwise it never gets compiled into the final binary?

Overview of MXMLC Pre-Processor Options

This is where the application compiler for Flex SDK, called mxmlc, comes into play. If you haven’t read the Adobe documentation for the Flex compilers, I recommend it. In fact this, article is a summary of just a few of the useful features found in that document. So, refer to the full Adobe documentation for further details!

I’m not covering any one specific set of tools that might utilize the Flex SDK, and so my methods should work for any system which calls mxmlc directly. I personally use FlashDevelop, and so I will have a short section on how to do edit compiler settings.

The application compiler allows several options related to conditional compilation. You can create defines much like in C, but these are set as compiler options versus being a pre-processor command in your Actionscript source code. It’s better to think of these options as constants used by your code, instead of as anything as fancy as DEFINE macros. A potential gotcha to look for is that when you reference a constant in your code, then the compiler will always try to resolve that name at compile time. So once you start using these constants, you will always need to satisfy their definition.

To define a constant, you simple pass the option “-define” to the compiler. The syntax is as follows:

1
-define=namespace::constant_name,value

You must always use the namespace::name format, as this helps alleviate namespace collisions between the native AS3 namespace and your own constants. You can use any name space that you want, such as DEBUG for various debug configuration options.

In the case of condition compilation, the value must always a Boolean of true or false. However, later I will show that you can use other types of constants.

A note on the difference between -define=namespace::constant_name,value and -define+=namespace::constant_name,value: The plus sign indicates on the assignment that you are appending a constant to the chain of any pre-defined constants, otherwise you will be overriding all constants that were defined up to that point. This isn’t a concern if you are using the command line -define option by itself, but if are you mixing configuration files with defines, then you should be aware of the difference. See the section on configuration files to clarify this behavior.

How to Conditionally Compile Blocks of Code

Now that we have a constant defined, how do we conditionally compile a segment of code?

That is fairly easy; There is a special syntax for wrapping blocks in the conditional. For example:

1
2
3
4
5
6
// This only is compiled into the binary if CONFIG::debug resolves as "true"
CONFIG::debug
{
var result:String = doSomethingOnlyInDebug() ;
label.text = result ;
}

It isn’t necessary to mark an entire block. A single statement can be conditionally compiled, such as:

1
2
CONFIG::debug
trace ("This is a trace message that we only want to appear in debug mode.") ;

Note that you can only include blocks of code, never exclude. But this isn’t much of a limitation, as you can simply use overlapping blocks of code when you want to define different behavior. (The Flex mxmlc documentation has an example of this.) Or, if you don’t care about speed or runtime type checking, you could use dynamic classes and prototyping to tailor a class for a particular implementation.

1
2
3
4
5
// Inside a class somewhere...
prototype.getType = function ():String { return "Normal" ; }

CONFIG::debug
prototype.getType = function ():String { return "Overridden by Debug option!" ; }

This is a pretty awful and contrived example, but hopefully it will give you ideas on how to work around the limitations of conditional compiles.

Using Configuration Files to Group Defines

The mxml compiler also lets us load additional XML configuration files in order to skip lengthy command line parameters. Besides the benefit of brevity, we also gain the ability to create modular configurations which can be swapped in and out in order to create different builds.

You can either override or include additional configuration files by using the “-load-config” command line parameter. For example:

1
-load-config+=build/options/release-build.xml

This will load and parse the release-build.xml, adding our own configuration settings. In this file, we can define constants in the same manner as using the command line. Here is the contents of release-build.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="utf-8"?>
<flex-config>
<compiler>
<define append="true">
<name>CONFIG::debug</name>
<value>false</value>
</define>
<define append="true">
<name>CONFIG::release</name>
<value>true</value>
</define>
<define append="true">
<name>CONFIG::logging</name>
<value>false</value>
</define>
<define append="true">
<name>CONFIG::debugtool</name>
<value>false</value>
</define>
</compiler>
</flex-config>

As you can see, we’re setting a lot of compile time constants, which will control a variety of behavior in our final binary program.

To Append Or Not

Why the use of append=”true” in the above example? You must realize how option ordering and configuration file precedence works in mxmlc for this to make sense.

Every assignment of a configuration file or option is considered final unless it uses the plus operator. For example, -load-config=myfile.xml will override all other configuration files, including flex-config.xml or any other xml file containing compiler configuration. By using -load-config+=myfile.xml, you are telling mxmlc to include this configuration alongside any others defined by pre-existing XML files (including the default flex-config.xml).

But the precedence logic doesn’t stop there. If you use a define statement in myfile.xml which defines a new constant, then unless you include append="true" then all other defines up to that point will be excluded. This can be used to your advantage if you want to completely override all pre-existing defined constants, but if you are using multiple configuration files you generally don’t want to do this. Your goal is just to add or change a few pre-existing constants, so using append is required.

The other benefit of using append=”true” is that you can override an existing constant. So in the above example, if CONFIG::debug was set to true in an earlier part of the mxml compile phase, then by appending CONFIG::debug=false we will override that setting.

Constants, Not Just Booleans

While conditional compilation requires the use of only Booleans, we can also define non-Boolean constants at compile time. Numbers and Strings data types are permitted. One obvious use for this is a version number that can be set at compile time.

Define PROGRAM::version as a string using this format:

1
-define=PROGRAM::version, "'1.0'"

Now, in your program, you can refer to this constant simple by doing this:

1
versionLabel.text = PROGRAM::version ;

Why do we use single and double quotes? This is because the String constant is sent as-is to the compiler. So if you didn’t include the single quotes, the line above would have parsed to this:

1
2
3
// Note the lack of quotation marks, indicating we're assigning a Number
// instead of a String
versionLabel.text = 1.0

This would result in the compiler error of “Implicit coercion of a value of type Number to an unrelated type String.”

You can, of course, use this same format in XML configuration files. The quotes are still required, but you no longer have to add the additional layer of quote indirection. For example:

1
2
3
4
<define>
<name>PROGRAM::version</name>
<value>"1.0"</value>
</define>

Working with FlashDevelop and mxmlc Options

This section is just for the FlashDevelop users!

In order to define single defines, you can utilize the compiler options pane of your project properties. To access this, right-click on the root of your project in the Project tree and select “Properties.” From here, the “Compiler Options” pane is accessible.

While you can use the “-define” syntax in Additional Compiler Options, it is much more natural to enter constants in the Compiler Constants drop-down. Select it and you can enter your constants, one per line, using the namespace::constant_name,value syntax.

However, as I already mentioned, it might be easier to define groups of constants in configuration files. You can add these to the compilation chain by using the Additional Compiler Options drop-down to add the -load-config+=filename.xml options.

In either case, be sure to use the += syntax for the load-config or define option, otherwise you will override the auto-generated configuration options that FlashDevelop emits for each compile!

Conclusion

There are plenty of other tricks lurking in the Flex SDK compiler documentation. I will be sharing more in the future, but I hope introducing you to conditional compiling makse your build process a little bit easier!