Copyright © 2002 Southern Storm Software, Pty Ltd.
Permission to distribute copies of this work under the terms of the
GNU Free Documentation License is hereby granted.
struct
,
union
, and other special C types must be mangled to
conform with CLI conventions, but such mangling should still be
readable to a human debugging the compiler.
Note 1. Given the nature of C, it is always possible for a programmer to write code that depends upon platform-specific word sizes, endianness, and operating system facilities. Our goal is that C code written to commonly used C coding standards should not be aware of such platform differences.Some things are deliberately outside the scope of this ABI definition. We do not describe the facilities that are provided by the "libc" implementation, or the contents of standard header files, for example.
Note 2. This doesn't preclude the application programmer from using native code facilities such as PInvoke. But the compiler itself will not use such features to implement the ABI.
Note 3. If the ABI avoids vendor-specific naming, it is more likely to be adopted by other vendors.
In the sections below, we suggest extended syntax for the C language to enable access to CLI-specific features. This syntax is only a suggestion. Two compilers that use different syntax for the same feature can still interoperate if they translate their syntax into the same ABI conventions.
All extension keywords begin and end in "__
", following
standard C practice. We recommend that compiler vendors seriously consider
adopting the proposed keywords to make it easier to port source code from
one compiler to another.
Note: Some of the features described in this document haven't been fully implemented by Portable.NET's C compiler yet. This document is therefore subject to change.
int
's are 32 bits in size,
long
's and pointers are 64 bits in size.int
's, long
's, and
pointers are all 32 bits in size.
Note: A "Model 64" program will fail to work on a 128-bit CLI implementation, for the same reason that "Model 32" programs fail on 64-bit CLI implementations. When and if 128-bit CLI implementations become common-place, it will be easy to extend this ABI to include a "Model 128".When the compiler builds an object file, application, or library, it MUST tag the corresponding module with the memory model. For example, the following module is tagged as "Model 64":
Linkers can use the presence of this attribute to detect that a C application is being linked, rather than a C# application, and then modify their behaviour accordingly. For example, by adding additional libraries to the link that aren't normally required by C# applications..module test.exe .custom instance void [OpenSystem.C]OpenSystem.C.MemoryModelAttribute::.ctor (int32) = (01 00 40 00 00 00 00 00)
The two primary memory models are designed to mirror existing 64-bit and 32-bit CPU architectures. But sometimes the programmer will want to exactly match the memory model to the underlying operating system, to improve interoperability with native code. In the process, portability is sacrificed, so this must only be used when absolutely necessary.
To match an underlying operating system, the compiler chooses either
"Model 64" or "Model 32", based on the size of the system's
"void *
" type. The compiler then applies a number of
"model modifiers" to alter type alignment values to match the
system. These modifiers are as follows:
0x00000001
short
values are aligned on 1-byte boundaries,
rather than 2-byte boundaries.0x00000002
int
values are aligned on 1-byte boundaries,
rather than 4-byte boundaries.0x00000004
int
values are aligned on 2-byte boundaries,
rather than 4-byte boundaries.0x00000008
long long
values are aligned on 1-byte boundaries,
rather than 8-byte boundaries.0x00000010
long long
values are aligned on 2-byte boundaries,
rather than 8-byte boundaries.0x00000020
long long
values are aligned on 4-byte boundaries,
rather than 8-byte boundaries.0x00000040
float
values are aligned on 1-byte boundaries,
rather than 4-byte boundaries.0x00000080
float
values are aligned on 2-byte boundaries,
rather than 4-byte boundaries.0x00000100
double
values are aligned on 1-byte boundaries,
rather than 8-byte boundaries.0x00000200
double
values are aligned on 2-byte boundaries,
rather than 8-byte boundaries.0x00000400
double
values are aligned on 4-byte boundaries,
rather than 8-byte boundaries.0x00000800
long double
values are aligned on 1-byte boundaries.0x00001000
long double
values are aligned on 2-byte boundaries.0x00002000
long double
values are aligned on 4-byte boundaries.0x00004000
long double
values are aligned on 8-byte boundaries.0x00008000
long double
values are aligned on 16-byte boundaries.0x00010000
0x00020000
0x00040000
0x00080000
These modifiers describe how the actual memory model differs from either "Model 32" or "Model 64". Programs that are compiled with non-zero modifier values are unlikely to work on runtime engines that use a different combination of flags. Programs with zero modifier values should work on all runtime engines that support the memory model.
When the object file is generated, the modifier flags are written into
the "MemoryModel
" attribute declaration, as an optional
argument:
This indicates "Model 32 with 4-byte alignment of 64-bit integers and doubles". If the second parameter is not present, the default modifier flag value is zero..module test.exe .custom instance void [OpenSystem.C]OpenSystem.C.MemoryModelAttribute::.ctor (int32, int32) = (01 00 20 00 00 00 20 04 00 00 00 00)
MemoryModelAttribute
" example in the previous section
demonstrated the use of the "OpenSystem.C
" assembly, which
provides a number of classes for tagging C applications, and for implementing
ABI support facilities. The following summarises the important classes
in the "OpenSystem.C
" namespace:
IsConst
const
".IsFunctionPointer
BitFieldAttribute
WeakAliasForAttribute
StrongAliasForAttribute
InitializerAttribute
.cctor
methods), in that initializers are
guaranteed to be executed before main
.InitializerOrderAttribute
FinalizerAttribute
Finalize
"
methods in garbage-collected objects. The order of garbage-collected
object finalization is indeterminate with respect to C finalizers.FinalizerOrderAttribute
MemoryModelAttribute
OriginalNameAttribute
LongJmpException
setjmp
/longjmp
operations.Crt0
LongDouble
native float
" type. The standard C# base
class library lacks a suitable class. This class has
a constructor that takes a single "native float
"
argument, and an "Unpack
" method that returns
a "native float
" when applied to an instance.FloatComplex
, DoubleComplex
,
LongDoubleComplex
FloatImaginary
, DoubleImaginary
,
LongDoubleImaginary
CNameAttribute
FloatComplex
" is marked as
"float _Complex
".To simplify discussion, we will use an abbreviated syntax to describe attributes in CIL assembly code examples. For example, the memory model designation in the previous section can also be written as:
This abbreviation is for exposition purposes only. It isn't intended to suggest an alternative syntax for CIL assemblers..module test.exe .custom [OpenSystem.C.MemoryModel(64)]
Type | Model 64 Size/Align1 |
Model 32 Size/Align1 | Description |
void |
1/1 2 | 1/1 | Void type |
_Bool |
1/1 | 1/1 | 8-bit boolean value (C# "bool ") |
char |
1/1 | 1/1 | Signed 8-bit integer |
unsigned char |
1/1 | 1/1 | Unsigned 8-bit integer |
short |
2/2 | 2/2 | Signed 16-bit integer |
unsigned short |
2/2 | 2/2 | Unsigned 16-bit |
__wchar__ |
2/2 | 2/2 | 16-bit wide character value (C# "char ") |
int |
4/4 | 4/4 | Signed 32-bit integer |
unsigned int |
4/4 | 4/4 | Unsigned 32-bit integer |
long |
8/8 | 4/4 | Signed 64-bit or 32-bit integer |
unsigned long |
8/8 | 4/4 | Unsigned 64-bit or 32-bit integer |
long long |
8/8 | 8/8 | Signed 64-bit integer |
unsigned long long |
8/8 | 8/8 | Unsigned 64-bit integer |
float |
4/4 | 4/4 | 32-bit IEEE 754 floating-point |
double |
8/8 | 8/8 | 64-bit IEEE 754 floating-point |
type * |
8/8 | 4/4 | Pointer to "type " |
float _Complex |
8/4 | 8/4 | Complex number type based on float |
double _Complex |
16/8 | 16/8 | Complex number type based on double |
float _Imaginary |
4/4 | 4/4 | Imaginary number type based on float |
double _Imaginary |
8/8 | 8/8 | Imaginary number type based on double |
Note 1. These size and alignment values refer to the
primary memory model. The values may be different if there are non-zero
model modifier flags in effect.
Note 2. The size of "void
" is 1, to be
consistent with gcc.
In "Model 64", pointers are allocated 8 bytes of memory, and aligned
on an 8-byte boundary, even on platforms that only support 32-bit pointers.
The expression "sizeof(void *)
" will always return 8.
This behaviour is necessary to provide a consistent "struct
"
layout on all CLI implementations, as we will see in the following sections.
const
" and "volatile
" qualifiers are
represented using the "OpenSystem.C.IsConst
" and
"System.Runtime.CompilerServices.IsVolatile
" modifiers.
The following table provides some examples:
Declaration | Representation |
const int x; |
int32 modopt(OpenSystem.C.IsConst) x |
void * volatile y; |
void * modreq(System.Runtime.CompilerServices.IsVolatile) y |
const char *s; |
int8 modopt(OpenSystem.C.IsConst) * s |
char * const s; |
int8 * modopt(OpenSystem.C.IsConst) s |
The placement of the type modifier is important. A qualifier at the
outer-most level of a type applies to the field or variable. A qualifier
at an inner level applies to a referenced type. In the last example
above, the variable "s
" cannot be modified, but it points
at a string that can be modified. In the second last example, the
variable can be modified, but not the string.
The "IsVolatile
" modifier is required, to be consistent
with other CLI-compatible languages. The "IsConst
" modifier
is optional, because other CLI-compatible languages can safely ignore it
(the programmer on the other hand probably shouldn't ignore it).
Fixed types have a constant size and alignment. The expression
"sizeof(T)
" can be evaluated to a constant at compile time.
Dynamic types have a constant size and alignment, but these values are not known until runtime. Native types (described in a later section) are an example, as are C# value types.
Unknown types have no known size. An example is "char[]
",
which cannot be used as the type of a structure field, as its storage
size cannot be determined.
Traditional C compilers only have "fixed" and "unknown" types. One of
the goals of this ABI is to minimize the occurence of "dynamic" types
so that "struct
" layout can be computed efficiently.
struct A
") are converted into a value
type called "struct A
", with no namespace qualifier. This
value type is marked as having explicit layout, with pre-computed class
packing and size values. Each field within the structure has a
pre-computed offset. For example, on a "Model 64" system:
On a "Model 32" system, the structure would be encoded as:struct A { int item; struct A *next; }; .class public explicit sealed ansi 'struct A' extends System.ValueType { .pack 8 .size 16 .field [0] public int32 item .field [8] public 'struct A' * next }
Structures may only contain fields with "fixed" layout. Native structures (described later) can contain fields with both "fixed" and "dynamic" layout, but their usage is restricted..class public explicit sealed ansi 'struct A' extends System.ValueType { .pack 4 .size 8 .field [0] public int32 item .field [4] public 'struct A' * next }
If a structure contains sub-structures, they are converted into companion structure types:
As can be seen, anonymous structures are assigned a unique numeric code, and are encoded as nested types. The code is unique to the surrounding structure, so that the same code will be generated each time the program is compiled.struct A { int x; struct { int y; } z; struct B { int w; } v; }; .class public explicit sealed ansi 'struct A' extends System.ValueType { .pack 4 .size 12 .field [0] public int32 x .class public explicit sealed ansi 'struct (1)' extends System.ValueType { .pack 4 .size 4 .field [0] public int32 y } .field [4] public valuetype 'struct A'/'struct (1)' z .field [8] public valuetype 'struct B' v } .class public explicit sealed ansi 'struct B' extends System.ValueType { .pack 4 .size 4 .field [0] public int32 w }
Note: It would be desirable to allow the runtime engine to perform structure layout dynamically, rather than fix types to specific sizes and fields to specific offsets. Readers who think it may be possible to do so may like to ponder how to efficiently compile the following code so that it will work regardless of the runtime size of "void *
" and the runtime alignment of "y
":
struct item { char x[sizeof(void *)]; long long y; }; long long get_y(struct item *i) { return i->y; }
union A
") are represented as value types with
the name "union A
", and all fields explicitly laid out to
start at offset 0.
Unions may only contain fields with "fixed" layout.union A { int x; double y; } .class public explicit sealed ansi 'union A' extends System.ValueType { .pack 8 .size 8 .field [0] public int32 x .field [0] public float64 y }
struct A { int x : 8; int y : 1; unsigned int z : 16; int w; } .class public explicit sealed ansi 'struct A' extends System.ValueType { .custom [OpenSystem.C.BitField("x", ".bitfield-1", 0, 8)] .custom [OpenSystem.C.BitField("y", ".bitfield-1", 8, 1)] .custom [OpenSystem.C.BitField("z", ".bitfield-2", 0, 16)] .pack 4 .size 12 .field [0] public int32 '.bitfield-1' .field [4] public unsigned int32 '.bitfield-2' .field [8] public int32 w }
A[]
" are mapped to a value
type called "array A[]
". For example, "int[]
"
is encoded as follows:
The value type must have a field called ".class public explicit sealed ansi 'array int[]' extends System.ValueType { .pack 4 .size 0 .field private static specialname int32 elem__ }
elem__
", which
defines the element type, and it must have the attributes
"private static specialname
".
Note. It will be rare to find a type of the form "A[]
" in a generated object file, because such types normally decay to pointer types when used as function arguments. The encoding is specified here because the compiler does need to distinguish "A[]
" from "A *
" in certain circumstances.
If the array type includes a non-zero size value, then it is encoded
as a value type with an explicit size defining the total size of the
array. For example, "int[100]
is encoded as follows:
The size of the array is determined by dividing ".class public explicit sealed ansi 'array int[100]' extends System.ValueType { .pack 4 .size 400 .field [0] public specialname int32 elem__ }
.size
"
by the size of the element type. The "elem__
" field in
this case must be "public specialname
". Arrays with
a zero size are encoded as follows:
Here, the ".class public explicit sealed ansi 'array int[0]' extends System.ValueType { .pack 4 .size 0 .field [0] public static specialname int32 elem__ }
elem__
" field is "static
". This
type can be distinguished from the encoding for "int[]
"
because the "elem__
" field is "public
"
instead of "private
".
Array element types must have "fixed" layout. The programmer can
allocate arrays of "dynamic" types using "malloc
"
or "alloca
".
The following is an example of encoding the two-dimensional array type
"int [300][400]
":
.class public explicit sealed ansi 'array int[300][400]' extends System.ValueType { .pack 4 .size 480000 // == 300 * 400 * 4 .field [0] public specialname valuetype 'array int[400]' elem__ } .class public explicit sealed ansi 'array int[400]' extends System.ValueType { .pack 4 .size 1600 // == 400 * 4 .field [0] public specialname int32 elem__ }
All of the types that are described in this section have "dynamic" layout. They cannot be used as array element types, or as the members of non-native structures and unions.
The use of these native types is highly discouraged, except where it is absolutely essential to interoperate with other system components:
Type | Description |
__native__ int |
Signed native integer (C# "IntPtr ") |
unsigned __native__ int |
Unsigned native integer (C# "UIntPtr ") |
long double |
Native floating-point |
long double _Complex |
Complex number type based on long double |
long double _Imaginary |
Imaginary number type based on long double |
The native integer types are "__native__ int
" and
"unsigned __native__ int
". They may be either 4 or 8 bytes in
size, depending upon the underlying platform. The expressions
"sizeof(__native__ int)
" and
"sizeof(unsigned __native__ int)
" are evaluated at run time.
The native floating point type is "long double
", and
is guaranteed to have precision greater than or equal to "double
".
The expression "sizeof(long double)
" is computed at runtime.
Structures can be specified to have native layout at declaration time:
struct __native__ A { int item; struct A *next; };
This is represented by a sequential type definition in the program's metadata:
The runtime engine will lay this out using platform-specific type sizes and alignment. The expression ".class public sequential sealed ansi 'struct A' extends System.ValueType { .field public int32 item .field public 'struct A' * next }
sizeof(struct A)
" will be
evaluated at runtime.Unions can also be specified to have native layout at declaration time:
The type is declared explicit, so that all fields can be defined with an offset of zero, but the type does not have an overall size.union __native__ A { int x; void *y; } .class public explicit sealed ansi 'union A' extends System.ValueType { .field [0] public int32 x .field [0] public void * y }
Types with "fixed" and "dynamic" layout may be used as the members of native structures and unions.
It is recommended that the compiler issue a warning when bit fields are used in native structures and unions, and the memory model does not have an appropriate memory model modifier set. The compiler's bit order may not match the native platform's bit order, leading to problems with PInvoke'd functions.
OpenSystem.C.IsFunctionPointer
" modifier:
void (*func)(int); .field public static method void * (int32) modopt(IsFunctionPointer) func
An array argument to a function will be converted into its "decayed" pointer form. For example:
int main(int argc, char *argv[]) { ... } .method public static int32 main (int32 argc, int8 * * argv) cil managed { ... }
Functions that take a variable number of arguments must be declared
with "vararg
" calling conventions:
int printf(const char *format, ...) { ... } .method public static vararg int32 printf (int8 modopt(IsConst) * format) cil managed { ... }
When arguments are passed to a variable-argument function, they must be converted into their "natural passing type" first:
Type | Natural Passing Type |
_Bool |
_Bool |
char |
int |
unsigned char |
int |
short |
int |
unsigned short |
int |
__wchar__ |
int |
int |
int |
unsigned int |
int |
__native__ int |
long |
unsigned __native__ int |
long |
long |
long |
unsigned long |
long |
long long |
long long |
unsigned long long |
long long |
float |
double |
double |
double |
long double |
OpenSystem.C.LongDouble |
type * |
long |
struct and union |
Same as input type |
Natural passing types help to properly implement cases where a value is passed as unsigned, but unpacked as signed, or is passed using a smaller type than the unpacking type.
The compiler must convert all variable arguments to their natural passing
types at the point of the call. The "va_arg
" operator is then
responsible for casting the natural passing type back to the programmer's
requested type.
The "va_list
" type is implemented by the C#
"System.ArgIterator
" class, and has "dynamic" layout.
The runtime engine will throw an exception if an attempt is made to
unpack an argument using the wrong natural passing type.
<Module>
"
type. However, there are some "undefined" issues that we now
deal with.
<Module>
"
type within a foreign assembly. This appears to be a hard-wired constraint.
Other CLR's (e.g. Portable.NET) make no distinction between the module type
and all other types.
To achieve interoperability with Microsoft's CLR, library assemblies must
use the "$Module$
" type for their global field and method
definitions instead of "<Module>
". The
"$Module$
" type must have the "public
" and
"sealed
" flags.
Executables still use the "<Module>
" type, as it appears
to work in all CLR's that have been tested so far. The
"<Module>
" type should have the "public
"
and "abstract
" flags.
When the assembler sees a dangling reference to something in the
"<Module>
" class, it will convert it into a member
reference on the "<ModuleExtern>
" class. For example:
If.method public static void hello() cil managed { call void hello2() }
hello2
remains undefined at the end of the assembly
process, then the resulting object file will look like this:
When the linker loads this object file, it will resolve references to ".method public static void hello() cil managed { call void '<ModuleExtern>'::hello2() }
<ModuleExtern>
" by looking for a matching definition
and changing the type reference appropriately. The new reference
may be to the linked executable's "<Module>
" type,
or to a foreign library's "$Module$
" type.
The "<ModuleExtern>
" type will itself be dangling.
The exact means by which this is accomplished is compiler-dependent, as the
ECMA specification does not define an object file format for the CLI.
Portable.NET's assembler encodes dangling types as a
TypeRef, scoped to the current module, but with no corresponding TypeDef.
The object file format is based on the native PE/COFF object file format,
with CIL metadata stored in the ".text$il
" section.
Portable.NET's linker fixes up dangling TypeRef's at link time.
static
" are converted
into "private
" fields or methods within the
"<Module>
" object file's class. All other variables
or functions are converted into "public
" definitions.
If the "<Module>
" class has any "public
"
members, then the class will also be declared "public
".
This ensures that a library will export its definitions correctly to
applications that link against the library.
private
" definition for the same function
or variable. Alternatively, one may be "private
" and
the other "public
".
We resolve this situation by renaming one of the "private
"
definitions to something else, and then redirecting all references to
the original to the renamed version. From an external user's point of
view, the "public
" definition (if any) will become the
visible definition. For example:
File 1:If two or more object files have conflicting ".field public static int32 xFile 2:.field private static float64 x .method public static float64 getx() cil managed { ldsfld float64 x ret }Result:.field public static int32 x .field private static float64 'x-1' .method public static float64 getx() cil managed { ldsfld float64 'x-1' ret }
public
"
definitions for a function or variable, then a linker error will occur.
Structure, union, and array types may also conflict when two
object files are linked together. In most cases, the two definitions
will be the same, because the same type is being used in both object
files (e.g. "struct _IO_FILE
" in glibc's stdio implementation).
When two types have identical definitions, the linker will copy one into the output file and ignore the other. When the two types have different definitions, the linker chooses one to become the primary copy, and the other is renamed.
If one of the types has the same definition as a type from a library, the linker should favour the library's definition, as it is the most likely candidate. If neither definition duplicates a library definition, the linker can choose either one, and probably should also report a warning to the programmer.
When program items are renamed, the resultant binary will not be in
sync with the source code. This can make source-level debugging
difficult. To alleviate this problem, the linker can add
"OriginalName
" attribute values to all renamed items:
Normally this is only required if an object file contained debug symbol information prior to renaming..field public static int32 x .field private static float64 'x-1' .custom [OpenSystem.C.OriginalName("x")] .method public static float64 getx() cil managed { ldsfld float64 'x-1' ret }
getuid
" function (paraphrased a little):
This will be compiled as follows:int __getuid(void) { ... } weak_alias(__getuid, getuid)
When a program is linked against this definition, the ".method public static int32 __getuid() cil managed { ... } .field public specialname static .method int32 * () 'getuid-alias' .method public static int32 getuid() cil managed { .custom [OpenSystem.C.WeakAliasFor("__getuid")] .maxstack 1 ldsfld .method int32 * () 'getuid-alias' tail. calli int32 () ret } .method private specialname static void '.init-1'() cil managed { .custom [OpenSystem.C.Initializer] .maxstack 1 ldftn void __getuid() stsfld .method int32 * () 'getuid-alias' ret }
WeakAliasFor
" attribute is used to redirect the
reference to the actual definition if the system does not contain
any other definitions for the function.
When a library that does not supply its own "getuid
"
is linked against this definition, the "getuid
"
method is called directly, which will then redirect control to
the actual "getuid
".
A program or library that defines its own "getuid
"
is compiled as normal:
At link time, the linker will insert an initializer which updates the ".method public static int32 getuid() cil managed { ... }
getuid-alias
" field with the new value:
where ".method private specialname static void '.init-1'() cil managed { .custom [OpenSystem.C.Initializer] .maxstack 1 ldftn void getuid() stsfld .method int32 * () [library]'$Module$'::'getuid-alias' ret }
library
" is the name of the library that defines
the "getuid-alias
" variable.Strong aliases for functions are defined in a similar manner:
In this case, whenever the linker sees a reference to ".method public static vararg int32 _IO_printf (int8 modopt(OpenSystem.C.IsConst) *format) cil managed { ... } .method public static vararg int32 printf (int8 modopt(OpenSystem.C.IsConst) *format) cil managed { .custom [OpenSystem.C.StrongAliasFor("_IO_printf")] }
printf
",
it will redirect the caller to "_IO_printf
". The body of
the alias function is empty, because it will never be called at runtime.Global variables may also have strong aliases associated with them:
When the linker sees a reference to "char **__environ; strong_alias(__environ, environ); .field public static int8 * * __environ .field public static int8 * * environ .custom [OpenSystem.C.StrongAliasFor("__environ")]
environ
", it will
substitute "__environ
".
Weak aliases are not supported for global variables. Weak aliases
exist in libc libraries primarily for legacy reasons. There are
existing C programs that depend upon variables like "environ
",
"timezone
", etc, being weak aliases, but they are rarer
than programs that depend upon functions being weak aliases.
It is recommended that if the compiler sees a weak alias definition for a variable that it output a strong alias instead.
specialname
" flag, have no parameters or return
values, and are marked with the "Initializer
"
attribute.
Finalizers are compiled into static methods that have the
"specialname
" flag, have no parameters or return
values, and are marked with the "Finalizer
" attribute.
The linker collects up all initializers and finalizers in a program or library and does the following:
public
methods in the
"<Module>
" class: ".init
"
and ".fini
"..init
" method calls the ".init
"
methods of all libraries that the program or library itself
depends upon..init
" method then calls all of the
locally-defined initializers..fini
" method calls all of the locally-defined
finalizers..fini
" method then calls the ".fini
"
methods of all libraries that the program or library itself
depends upon, in reverse order..init
" methods are called is usually
indeterminable. The compiler can alter the ordering using the
"InitializerOrder
" attribute:
This initializer will be executed before all "normal" initializers, which have a default order value of zero..method private specialname static void '.init-1'() cil managed { .custom [OpenSystem.C.Initializer] .custom [OpenSystem.C.InitializerOrder(-1)] ... }
The "FinalizerOrder
" attribute can used to alter the
ordering of finalizers. A finalizer with an order value of -1 will
be executed after the normal finalizers.
When the linker generates the ".init
" and ".fini
"
methods, it must also insert some reference counting code. The body
of the ".init
" method will only be executed upon the first
call, and the body of the ".fini
" method will only be
executed upon the last call. Appendix A contains some sample code
that demonstrates this.
Usually, locally-defined initializers and finalizers are declared
"private
". The renaming logic described in a previous
section will take care of resolving ambiguities in naming.
main
" function is compiled,
a small amount of CIL code is added to define the application entry point.
This code calls facilities in the "OpenSystem.C.Crt0
" class
to initialize the application, to invoke "main
", and to
handle shutdown tasks when "main
" exits. Using C# syntax,
the startup code looks like this:
public static void .start(String[] args) { try { int argc; IntPtr argv; IntPtr envp; argv = Crt0.GetArgV(args, sizeof(void *), out argc); envp = Crt0.GetEnvironment(); Crt0.Startup("libcNN"); Crt0.Shutdown(main(argc, argv, envp)); } catch(OutOfMemoryException) { throw; } catch(Object e) { throw Crt0.ShutdownWithException(e); } }where "
libcNN
" is the name of the "libc" implementation
that the program was compiled against. This will normally be
"libc64
" for "Model 64" and "libc32
" for
"Model 32". The compiler will only pass those parameters to
"main
" that the programmer specified in their source code.
The startup code in the application is kept deliberately simple,
with most of the real work being done in the "OpenSystem.C.Crt0
"
class. This allows the crt0 code to be modified to accomodate new "libc"
requirements in the future, without needing all existing applications
to be recompiled.
.class private sealed '.init-count' extends System.Object { .field private static int32 count } .method public specialname static void '.init'() cil managed { .maxstack 2 .locals (class System.Type) // Lock down '.init-count' to synchronize access. ldtoken '.init-count' call class System.Type System.Type::GetTypeFromHandle (valuetype System.RuntimeTypeHandle) dup stloc 0 call void System.Threading.Monitor::Enter(class System.Object) .try { // Increase the reference count, and check for the first call. ldsfld '.init-count'::count dup ldc.i4.1 add stsfld '.init-count'::count brtrue L1 leave runinit L1: leave exit } finally { ldloc 0 call void System.Threading.Monitor::Exit(class System.Object) endfinally } runinit: // Run the initializers for the libraries. call void [libc64]'<Module>'::'.init'() // Run the local initializers. call void '<Module>'::'.init-1'() call void '<Module>'::'.init-2'() ... call void '<Module>'::'.init-N'() exit: // Initialization has finished. ret } .method public specialname static void '.fini'() cil managed { .maxstack 2 .locals (class System.Type) // Lock down '.init-count' to synchronize access. ldtoken '.init-count' call class System.Type System.Type::GetTypeFromHandle (valuetype System.RuntimeTypeHandle) dup stloc 0 call void System.Threading.Monitor::Enter(class System.Object) .try { // Decrease the reference count, and check for the last call. ldsfld '.init-count'::count ldc.i4.1 sub dup stsfld '.init-count'::count brtrue L1 leave runfini L1: leave exit } finally { ldloc 0 call void System.Threading.Monitor::Exit(class System.Object) endfinally } runfini: // Run the local finalizers. call void '<Module>'::'.fini-1'() call void '<Module>'::'.fini-2'() ... call void '<Module>'::'.fini-N'() // Run the finalizers for the libraries. call void [libc64]'<Module>'::'.fini'() exit: // Finalization has finished. ret }