Defining and Using Preprocessor Directives in the Mobility Pack
Contributed by Jirka
Prazak. Maintained by Anatole Wilson
One of the most difficult aspects of developing applications for mobile devices
is device fragmentation. Mobile devices often differ in a variety of
attributes, such as screen size or color depth, and many support proprietary
or optional APIs. These differences can require special code or project settings.
The NetBeans Mobility Pack solves this problem by letting you use project configurations
and preprocessor directives within your code, so you can tailor your MIDlet
for multiple devices within a single source file.
For example, if you are writing an application targeted for several different
devices, you can create a project configuration for each device, then add preprocessor
directives that optimize the code for each configuration. When you build and
run your MIDlet, you can choose to build for one or all of the configurations,
producing a distribution JAR for each configuration.
This article provides a guide to the syntax of the preprocessor used in the
Mobility Pack's device fragmentation solution. This guide is divided into the
following sections.
Using Preprocessor Directives
Preprocessor directives are specified by creating a commented line that starts
with the //# character sequence immediately followed by the directive,
for example //#ifdef.
Preprocessor blocks must be well-formed, which means when a block
is started with one of the //#if directives, it must be closed
by an //#endif directive. Blocks can be nested, which means that
inside an if/elif/else/endif block can be arbitrary number of additional
if/elif/else/endif blocks.
For example:
//#if mmedia
//#if nokia
//#if s60_ver=="1.0"
import com.nokia.mmapi.v1
//#elif s60_ver=="2.0"
import com.nokia.mmapi.v2
//#else
import com.nokia.mmapi.def
//#endif
//#else
import javax.microedition.mmapi
//#endif
//#endif
Directive Syntax and Functions
The following sections are a detailed
description of the identifiers and expressions you can use in preprocessor directives,
and their syntax.
Identifiers and Expressions
The following identifiers and expressions are available in the described
syntax and function, as follows:
-
#ifdef identifier
The identifier represents a variable of any type (Boolean, String, Integer)
and checks whether or not it is defined. If it is, then the result is "True"
and the code that follows will be left alone. Any nested blocks will be
processed as well. Otherwise, code that follows will be commented out, and
any nested blocks will not be evaluated. The #ifdef identifier
must be closed with #endif. This commenting behavior is same
for all of the other directives.
-
#ifndef identifier
Works as #ifdef but returns the "Not" boolean value
of that result. The #ifndef identifier must be closed with
#endif.
-
#elifdef identifier
Works as a standard "else if" statement, but automatically checks
whether or not the identifier is defined. Can only complement inside blocks
started by #ifdef or #ifndef.
-
#elifndef identifier
Works as a standard "else if" statement, but automatically checks
whether the identifier is not defined. Can only complement inside blocks
started by #ifdef or #ifndef.
-
#if expression
Evaluates an expression passed to it and fires the appropriate action. How
expressions are evaluated is explained bellow. Must be closed with #endif.
-
#elif expression
Works as a standard "else if" statement and can complement only
in blocks started by an "if" statement. The #elif
expression preprocesses the code that follows based on the result of the
expression.
-
#else
Works as a standard "else" and will only preprocess the code that
follows when none of the previous conditions in the defining block are true.
Complements inside any block started with an #if/#ifdef/#ifndef
directive.
-
#endif
This directive must be used to close any block started with #if/#ifdef/#ifndef.
-
#condition expression
Must be on the first line in a file. This directive determines if the file
should be included in the build based on the result of the expression.
-
#debug level
Determines if the line following the directive should be commented or uncommented
based on the debug level set in project properties. If the level is omitted
and the debug level is not set to "Off" in the project properties,
it will automatically debug the line in question. #debug level
is used for debugging purposes in conjunction with, for example, System.out.println.
#debug level can be nested.
-
#mdebug level
Behaves the same as #debug, but will uncomment or comment a
whole block of lines following the line it is on until it reaches #enddebug.
is used for debugging purposes in conjunction with, for example, System.out.println.
#mdebug level can be nested. If an #mdebug block
partially intersects an if/ifdef/ifndef block (e.g. enddebug
is outside a closed if block in which mdebug is called) the preprocessor
will generate errors.
-
#enddebug
Must terminate an #mdebug block.
-
#define identifier or identifier=value or identifier
value
Systematically adds temporary abilities or variables in real time to the
preprocessor memory. Nested block insensitive. Global variables defined
in the configuration properties override these temporary variables.
-
#undefine identifier
Removes temporary abilities or variables from the memory. #undefine
can also be used to remove global variables defined in the configuration
properties from the preprocessor memory but will not remove them
from the list of project or configuration variables.
Variables
The preprocessor supports three types of variables: Strings, Integers and
Booleans.
The variable names must start with characters which are the same as the start
characters of valid Java identifiers but in addition, consequentive characters
can also be '.' '\' and '/'. This is
to make migration easier for developers with existing Antenna or J2ME Polish
sources which happen to use these characters in some variable and symbol names.
Most common comparisons <=, <, >=, > and ==
are supported between the variables. Comparisons should not be done on different
variable types. If different variable types are compared, the IDE issues a
warning (an annotation in the Editor and a warning in task output), but the
expression gets evaluated according to the behavior described in the Comparison
Operators section.
Strings can contain just about anything but must be enclosed in quotation
marks (""). Checks for definition can be made on any
variable and, unlike J2ME Polish, no additional keywords are needed. The preprocessor
checks for the J2ME Polish :defined construct and removes it
from the variable name before checking for the definition. For example, let
string nokia_model be "N60", then //#ifdef nokia_model
or //#if nokia_model will yield "True" because it is
defined and is evaluated as such. On the other hand, //#if nokia_model=="7610"
will yield "False" because nokia_model is not "7610".
Here is what it would look like in the source code in J2MEPolish:
//#define nokia_model="N60"
//#ifdef nokia_model:defined
System.out.println("Nokia");
//#else
System.out.println("Other");
//#endif
In NetBeans, it would like like this:
//#define nokia_model="N60"
//#ifdef nokia_model
System.out.println("Nokia");
//#else
System.out.println("Other");
//#endif
Integers are numbers and behave the same as strings when it comes to boolean
operations. However, they are compared as true integers, and can be used for
such tasks as preprocessing code where various screen resolutions require
different images that are optimized for different resolutions. For example,
//#if screen_width>100 && screen_height>120 can specify
a code block which will import images only for devices with screens larger
than 100x120.
If the variable is an empty variable (for example, a configuration name)
then it is considered a define symbol. If the variable is not defined anywhere
in preprocessor scope (as an ability or configuration) then it is considered
an undefined symbol.
Operators
There are three types of operators with different priorities:
! or the "Not" Operator
! or the"Not" Operator has the highest priority and can be used
on variables and expressions, such as !<identifier> !<expression>.
For example, //#if !nokia will check whether nokia is not defined.
//#if !(screen_width>100 && screen_height>120)
will check if screen size is smaller than 100x120. The expression must be
a valid one enclosed in parenthesis, as shown. Since the ! operator
has the highest priority, expressions such as //#if screen_size=="100x200"
is illegal and will yield syntax errors because a boolean result cannot be
compared to a string.
The Comparison Operators
The comparison operators have the second highest priority and perform typical
comparison operations. They can compare strings lexically and integers mathematically.
Cross-type comparisons are supported and behave as defined in Variable Comparisons
Table. They can be used only in expressions and should compare two variables,
not symbols.
There is also a special comparison operator which performs a "subset"
relationship operation. This operator is denoted by the @ token,
and both left and right arguments should be strings which represent two sets
of tokens delimited by specific delimiters. It first tokenizes both left and
right string arguments into sets and then determines if the set from the left
argument is a subset of the set from the right argument. The valid word delimiters
are <whitespace>,',' and ';' and they can
be mixed arbitrarily within each argument. The comparison operator behaves
just as in the following examples:
"gif" @ "gif86, jpeg, gifaboo" = False
"gif" @ "gif gif86 jpeg" = True
"1 2 4;7,8" @ "0,1,2,3,4,5,6,7,8,9" = True
"3 5 7 11 13" @ "0,1,2,3,4,5,6,7,8,9" =
False
Even though the variables should be of the same type when comparing, the
preprocessor will not fail but will present a warning (an annotation in the
Editor and a warning in task output) and evaluate the expression as follows.
When the left and right sides of the comparison operation are of different
types, both will be considered strings and compared lexically. The exception
is the @ operation where the "subset" relationship
operation will still be performed (as such, you can check if a certain integer
is in a particular set). If one of the variables is not defined or is defined
as a boolean symbol, it will be considered an empty String "".
If one of the variables is an integer, it will also be converted to a string
and a lexical comparison will take place. You should avoid comparing variables
of different types, but doing so by accident will not break the build process.
Boolean Operators
Boolean operators have the lowest priority relative to the rest of the operators,
but they do have different priorities amongst each other, just like in the
Java language. They perform typical logical operations such as &&,
|| and ^ on booleans, expression results, or check
for variable definitions, and then treat those as booleans as well. The &&
operator has the highest priority from all three boolean operators, while
^ and || have lower priorities, respectively.
For example, if we preprocess the following example with active configuration
being "Series40"
//#ifdef Series60
//#define tmpNokia3650
//#define tmpNokia3660
//#define tmpSiemensSX1
//#define tmpScreenWidth=176
//#define tmpScreenHeight=208
//#elifdef Series40
//#define tmpNokia3220
//#define tmpNokia3210
//#define tmpScreenWidth=80
//#define tmpScreenHeight=100
//#elifdef Series20
//#define tmpNokia8320
//#else
//#define default
//#endif
//#if tmpScreenWidth==176 && tmpScreenHeight=208 || tmpNokia3650 && tmpNokia3660
System.out.println("Series 60 configuration active!");
//#elif tmpScreenWidth==176 || tmpScreenWidth=80 && tmpScreenHeight=100 || tmpNokia8320
System.out.println("One of the Nokia configurations active!");
//#else
System.out.println("Default configuration active!);
//#endif
the result will be
//#if tmpScreenWidth==176 && tmpScreenHeight=208 || tmpNokia3650 && tmpNokia3660
//# System.out.println("Series 60 configuration active!");
//#elif tmpScreenWidth==176 || tmpScreenWidth=80 && tmpScreenHeight=100 || tmpNokia8320
System.out.println("One of the Nokia configurations active!");
//#else
//# System.out.println("Default configuration active!);
//#endif
Expressions
Expressions are evaluated according to the following Backus Naur Form (BNF)
grammar specification:
<expression>::= <term> { <boolop> <term> }
<term>::= <factor> { <compop> <factor> }
<factor>::= notop <factor> | value | <expression>
<boolop>::= && | || | ^
<compop>::= > | >= | < | <= | == | != | @
<notop>::= !
In the table below, assuming that the following variables are in scope of
the current configuration:
| Variable Name |
Variable Type |
Value |
nokia |
Boolean |
null |
screen_width |
Integer |
100 |
screen_height |
Integer |
160 |
symbVar |
String |
v7.0 |
mmapi |
Boolean |
null |
Assuming the values listed above, here are some examples of legal and illegal
expressions:
| Expression |
Yields Value of |
!nokia && mmapi |
False |
nokia&&mmapi |
True |
symbVer=="v7.0" || (screen_width>=100 &&
screen_width>=100) |
True |
siemens && !screen_width!=100 |
Syntax Error |
siemens || !nokia |
False |
nokia && (screen_width>100 || screen_height>100 |
Syntax Error |
You can use the following table to understand how the preprocessor evaluates
variable comparisons. For example, if the variable on the left side of the
comparison is an integer and the variable on the right side is a string, then
using the == operator will be evaluated lexically. If the same
variables are compared with the @ operator, the comparison will
be evaluated mathematically.
Left Side |
Right Side |
== |
!= |
> |
< |
>= |
<= |
@ |
| undefined |
undefined |
warn |
warn |
warn |
warn |
warn |
warn |
warn |
| undefined |
defined |
warn |
warn |
warn |
warn |
warn |
warn |
warn |
| undefined |
string |
warn |
warn |
warn |
warn |
warn |
warn |
warn |
| undefined |
integer |
warn |
warn |
warn |
warn |
warn |
warn |
warn |
| defined |
undefined |
warn |
warn |
warn |
warn |
warn |
warn |
warn |
| defined |
defined |
warn |
warn |
warn |
warn |
warn |
warn |
warn |
| defined |
string |
warn |
warn |
warn |
warn |
warn |
warn |
warn |
| defined |
integer |
warn |
warn |
warn |
warn |
warn |
warn |
warn |
| integer |
undefined |
warn |
warn |
warn |
warn |
warn |
warn |
warn |
| integer |
defined |
warn |
warn |
warn |
warn |
warn |
warn |
warn |
| integer |
string |
lex |
lex |
lex |
lex |
lex |
lex |
math |
| integer |
integer |
math |
math |
math |
math |
math |
math |
warn |
| string |
undefined |
warn |
warn |
warn |
warn |
warn |
warn |
warn |
| string |
defined |
warn |
warn |
warn |
warn |
warn |
warn |
warn |
| string |
string |
lex |
lex |
lex |
lex |
lex |
lex |
math |
| string |
integer |
lex |
lex |
lex |
lex |
lex |
lex |
warn |
Compilation Based on Device Platform Versioning
On occasion device lines from certain manufacturers have various platforms
and platform versions. In this example, we take Nokia and their s60 and s40
platforms, both with version 1 and 2. To accomplish this, we create four configurations
and add abilities as follows:
| Configurations |
Nokia.s60.v2 |
Nokia.s60.v1 |
Nokia.s40.v2 |
Nokia.s40.v1 |
| Abilities |
manufacturer=Nokia
platform=s60
platform_version=2 |
manufacturer=Nokia
platform=s60
platform_version=1 |
manufacturer=Nokia
platform=s40
platform_version=2 |
manufacturer=Nokia
platform=s40
platform_version=1 |
When the symbols and variables are properly defined, the code that will compile
for all four platforms based on their differences could look like:
//#if manufacturer=="Nokia"
.
compile all code common to nokias, e.g. using FullCanvas
.
//#if platform=="s40"
.
add s40 specific code
.
//#if platform_version=1
.
compile s40.v1 specific stuff
.
//#elif platform_version=2
.
compile s40.v2 specific stuff
.
//#else
.
compile s40 generic
//#endif
.
.
//#elif platform=="s60"
.
add s60 specific code
.
//#if platform_version=1
.
compile s60.v1 specific stuff
.
//#elif platform_version=2
.
compile s60.v2 specific stuff
.
//#else
.
compile 640 generic
//#endif
.
.
//#else
.
add generic Nokia code
//#endif
//#endif
Under many circumstances, however, this approach might not be possible or
will need a lot of redundant code writing, especially when actual features
in the software depend on the platform specification. This approach will yield
good results when handling various audio/video formats, for example. When
functionality and features in the software are based on the platform specification,
a more incremental approach is desirable.
.
.
code common to all devices
.
.
//#if manufacturer=="Nokia" && platform=="s60"
&& platform_version==1
.
implement specific feature
.
//#elif manufacturer=="Nokia" && platform=="s60"
&& platform_version==2
.
.
//#elif manufacturer=="Nokia" && platform=="s40"
&& platform_version==1
.
.
//#elif manufacturer=="Nokia" && platform=="s40"
&& platform_version==2
.
.
//#endif
Depending on the situation, both approaches can be mixed together to get
the desired results. NetBeans provides some predefined abilities, such as
CLDC and MIDP versions, as well as symbols for various JSRs which can be used
and do not have to be defined by hand.