ilasm has a small preprocessor built into its lexer (the standard does not describe it). It works on tokens, not on lines: a # word may be anywhere between tokens, and the macro definitions are text substitutions of one token. It also has a few directives that are shorthand for things the language already has. None of this exists in the binary form, so ildasm does not write the preprocessor lines; its source has a routine for .typedef aliases and an option for .typelist.
The # directives
| Directive | Meaning |
|---|---|
#define NAME | defines NAME with no value (for #ifdef) |
#define NAME "text" | defines NAME as the text of the string, which is then read in place of every later token NAME as if it had been written there (a string that holds several tokens works) |
#undef NAME | removes a definition |
#ifdef NAME, #ifndef NAME | start a conditional part: read it only if the name is defined, or is not defined |
#else, #endif | the other branch of the condition, and its end; a part can hold more conditions, and an #endif without an opening condition is an error (“Unmatched #endif”) |
#include "file" | reads another file at that point; the name is in double quotes and is not an escaped string. A name without a directory is searched in the folders of the /INCLUDE=path option (separated by : on Unix and macOS, ; on Windows), then as written |
#line | source line information (see the lexical structure) |
A name is an ordinary Id. A part that a condition leaves out is skipped word by word: the lexer drops every blank-separated piece that is not a preprocessor word, comments are still recognised, and nothing else is read, so a " or ' that is never closed in a skipped part does no harm. A defined name inside a string or a comment is not replaced.
#define ASSEMBLY_NAME "loop3_d"
#define ZERO "0"
#ifdef DEBUG
.assembly extern mscorlib {}
#else
.assembly extern System.Runtime {}
#endif
.assembly ASSEMBLY_NAME {}
#include "loop3.il"
.method static void m() cil managed
{
ldc.i4 ZERO
pop
ret
}
The runtime’s own tests use this: many of their .il files are a #define ASSEMBLY_NAME "…" line and an #include of a shared body, so one body is assembled several times (for instance as debug and as release builds) under different assembly names. The viewer colours a defined name where it is used (as a constant) and, for a name defined in a file that is not read, when it is the operand of an instruction that takes a number (ldc.r4 POS_INF).
.typedef, .typelist and .mscorlib
| Directive | Meaning |
|---|---|
.typedef type as Name | gives a name to a type (.typedef class [mscorlib]System.Collections.Hashtable as HT); the same for a class reference, a member reference (a method or field: .typedef method void Foo::M() as Run), and a custom attribute with its blob. The name can then be used where the thing is wanted: newobj instance void HT::.ctor(), call Run, or an owner-less .custom that repeats the stored attribute |
.typelist { class class … } | lists the classes in an order, so that a round trip keeps the order of the type definitions; ildasm writes it with /TYPELIST |
.mscorlib | says that this assembly is the system library: System.Object and the like are then taken from the module itself and not from a reference |
.template | a word of the keyword table that no rule of the grammar uses |
; | an empty declaration; a semicolon is accepted wherever a declaration can start |
Not described
Known, not yet covered
- Reference: a command-line way to define names (an ilasm option for
#define) was not found in the usage text of ilasm that was read; the effect of an#includecycle and the way the environment stack nests are not described.