PInvoke Conventions for Unix

Rhys Weatherley, rweather@southern-storm.com.au.
Last Modified: $Date: 2001/08/13 05:22:07 $

Copyright © 2001 Southern Storm Software, Pty Ltd.
Permission to distribute unmodified copies of this work is hereby granted.

1. Introduction

The ECMA Common Language Infrastructure (CLI) specification defines a mechanism that can be used to invoke functions that are written in native code, as opposed to IL bytecode. This mechanism is typically referred to as "PInvoke" or "It Just Works".

Implementations of the Common Language Runtime (CLR) on Windows provide a rich set of tools to converting between the well-defined world of CLR conventions, and the ill-defined world of Windows API conventions. Attributes are added to method declarations that identify the DLL that contains the function, and the mechanism to use to convert CLR types into parameter types for the function. These attributes are converted into flags and token structures within the metadata of the program.

Implementations of the CLR on Unix will need a similar set of tools. However, the problem is exacerbated by the variations in CPU types that are common in the Unix community. Windows API's typically use types such as WORD and DWORD, which are fixed in size for all platforms. Unix API's typically use types such as int and long which are variable in size.

This document proposes a standard mechanism for "PInvoke" under Unix. The goal is to create a system that "just works", even where variations in type sizes occur.

2. Conventions

When referring to systems with different type sizes, we will use the following terminology:

32/32 systemint and long are both 32 bits in size. (Sometimes referred to as "ILP32")
32/64 systemint is 32 bits in size and long is 64 bits in size. (Sometimes referred to as "LP64")
64/64 systemint and long are both 64 bits in size.

We use the convention "native X" to refer to the native C type "X". e.g. "native unsigned int", "native char", etc.

3. Handling type size variations

The simplest example of where type sizes can vary is the Posix lseek system call. It is typically declared in C as some variant of the following:
extern long lseek(int fd, long offset, int whence);
On recent systems, the off_t typedef is used in place of long, but the underlying definition is the same.

If we used the obvious approach, we might be tempted to declare this in C# as:

[DllImport("libc")]
extern long lseek(int fd, long offset, int whence);
Here, we have reused the DllImport attribute to indicate the shared object from which to import the function. This will ensure that a PInvoke record is created in the metadata with the module reference set appropriately.

However, the above definition will not work on 32/32 systems (the most common at present). This is because the C# long type is 64 bits in length, not the 32 that the 32/32 C API expects. It also will not work on 64/64 systems because the C# int type is 32 bits in length, not the 64 that the 64/64 C API expects.

There are actually three "correct" definitions for lseek:

32/32 systems:
[DllImport("libc")]
extern int lseek(int fd, int offset, int whence);
32/64 systems:
[DllImport("libc")]
extern long lseek(int fd, long offset, int whence);
64/64 systems:
[DllImport("libc")]
extern long lseek(long fd, long offset, long whence);
Which definition we use is dependent upon the underlying operating system and CPU type. Any application that uses this declaration will need to be compiled in three different variants. This situation is untenable, especially if we intend to wrap large API's such as GTK+.

CLI allows attributes to be added to parameters to modify their marshaling characteristics. This is typically used to convert C# types into the foreign type systems used by the Win32 SDK and COM. One of the "unmanaged types" that can be specified is SysInt, which indicates an integer value of the correct native size for the underlying platform. We can thus modify the definition of lseek as follows:

[DllImport("libc")]
extern long lseek([MarshalAs(UnmanagedType.SysInt)] int fd, long offset, [MarshalAs(UnmanagedType.SysInt)] int whence);
Alternatively, the built-in value type IntPtr can be used (suggested by Jay Freeman):
[DllImport("libc")]
extern long lseek(IntPtr fd, long offset, IntPtr whence);
This takes care of the int parameters for the method. ECMA specifies UnmanagedType.SysInt and UnmanagedType.SysUInt, but there are no equivalents for the native long type. Also, it may not always work.

The native integer types used by the CLR are specified to be the most efficient integer sizes for the CPU in question. On a 64-bit CPU, this will normally be 64-bits, even though most 64-bit C compilers for Unix will define int to be 32-bit. Hence, it may not be possible to rely upon UnmanagedType.SysInt or IntPtr to provide the correct behaviour on all platforms.

Three different proposals seem to offer a solution to this problem. They are described in the following subsections.

3.1 NativeTypeSizes

A new attribute called NativeTypeSizes is introduced which can be associated with a method (the namespace for this type is yet to be determined). We would modify our lseek example as follows:
[DllImport("libc"), NativeTypeSizes(true)]
extern long lseek(int fd, long offset, int whence);
The presence of this attribute with a value of true indicates to a compliant CLR that types should be marshaled to their obvious native counterparts. We now have one definition that works for all Unix platforms without requiring multiple library and application builds.

If a method contains a mixture of fixed-size and variable-size parameters, then explicit marshaling can be applied to the fixed-size parameters. Consider the following C declaration (assuming an appropriate typedef for int32):

extern int foo(long x, int32 y);
In this case, we would declare the C# counterpart as follows:
[DllImport("libfoo"), NativeTypeSizes(true)]
extern int foo(long x, [MarshalAs(UnmangedType.I4)] int y);
The explicit marshaling attribute on y will override the default behaviour of marshaling to SysInt.

Bob Salita suggested hijacking the existing calling convention specifications. Windows allows methods to be labelled as "cdecl", "stdcall", or "winapi". We could hijack one of these values to indicate native type size handling. However, this could be confusing to programmers and may cause problems should we ever discover a legitimate need for the original meanings.

3.2. PosixProto

Another possibility, suggested by Miguel de Icaza, is to have a PosixProto attribute that specifies the full alternative prototype for the method, using Posix types. Miguel's version of the example was:
[DllImport("libc"), PosixProto(int, off_t, int)]
extern long lseek(int fd, long offset, int whence);
Here, PosixProto is an attribute with a variable number of parameters that takes type names as arguments. If we add the type for the return value (missing from Miguel's example), and make it strictly conforming to C# syntax, we would get something like the following:
[DllImport("libc"), PosixProto(typeof(off_t), typeof(int), typeof(off_t), typeof(int))]
extern long lseek(int fd, long offset, int whence);
where PosixProto and off_t are defined as follows:
public class PosixProto : Attribute
{
    public PosixProto(Type returnType, params Type[] paramTypes);
};

public struct off_t {}

Using this information, the runtime engine can convert between the engine type (e.g. long) and the underlying Posix type (e.g. off_t). The runtime engine is configured at compile time to know how to marshal between engine types and the standard Posix types.

The beauty of this system is that it can be extended to other ANSI C and Posix types, including the "nasty" ones such as struct stat. A drawback of this system is that the runtime engine must be pre-configured with all types of interest.

3.3. PosixType

A third possibility (due to Rhys Weatherley) is to allow the Posix types to be defined as real C# value types, and to annotate them with attributes that indicate special marshaling behaviour.

[PosixType]
public struct off_t
{
    private long value;

    public off_t(long x) { value = x; }

    public static operator implicit off_t(long x)
        { return new off_t(x); }
    public static operator implicit long(off_t x)
        { return x.value; }
};

[PosixType]
public struct int_t
{
    private int value;

    public int_t(int x) { value = x; }

    public static operator implicit int_t(int x)
        { return new int_t(x); }
    public static operator implicit int(int_t x)
        { return x.value; }
};

[DllImport("libc")]
extern off_t lseek(int_t fd, off_t offset, int_t whence);

The presense of the PosixType attribute on the value type definition indicates that special marshaling behaviour is required for PInvoke methods. The implicit conversions ensure that the resulting types act like the regular numeric types in ordinary usage.

This scheme can also be used to modify how larger structures are laid out. Consider the struct timeval type:

struct timeval
{
    long tv_sec;
    long tv_usec;
};
Because the size of long varies between systems, we will have problems with Posix functions such as gettimeofday. The CLR layout of the structure will not match the underlying C library. Using an appropriate definition of long_t, we can write this in C# as follows:
public struct timeval
{
    public long_t tv_sec;
    public long_t tv_usec;
};
Because long_t has the PosixType attribute, compliant CLR's know to lay out the field as a native long, rather than a full CLR long. The gettimeofday function can then be declared as follows:
[DllImport("libc")]
extern unsafe int_t gettimeofday(timeval *tv, timezone *tz);
There is one drawback of using types such as int_t and off_t. Values must be converted to and from normal CLR types using the implicit conversion operators. This involves some additional overhead in the bytecode that the interpreter must handle. However, JIT's should be able to optimize this overhead away by looking for references to Posix-enabled conversion operators.

3.4 PosixType and Posix struct's

Some Posix types are defined as struct's that vary widely between systems. One of the worst such offenders is the struct stat type. It isn't possible to define a C# struct using types such as int_t and long_t because some systems have entire fields missing, or different orders for the fields.

[PosixStructType]
public struct stat
{
    public dev_t st_dev;
    public ino_t st_ino;
    public mode_t st_mode;
    public nlink_t st_nlink;
    public uid_t st_uid;
    public gid_t st_gid;
    public dev_t st_rdev;
    public dev_t st_rdev;
    public off_t st_size;
    public blksize_t st_blksize;
    public time_t st_atime;
    public time_t st_ctime;
    public time_t st_mtime;
};
This definition does not match any known Unix flavour, including Linux: there are normally extra padding and unused fields. We've omitted these extra fields on purpose to help demonstrate how they are handled.

The presence of the PosixStructType attribute indicates that the runtime engine is responsible for laying out this structure. It adjusts the offsets and types of each field so that they correctly match those of the underlying struct stat type. Any extra fields are not accessible by the C# application.

The layout algorithm is based on field name. We have purposely reversed the usual order of the st_mtime and st_ctime fields above. The runtime engine will remap the field offsets so that they match the underlying type correctly.

Note: we did not place the PosixStructType attribute on the timeval definition in the previous section. If the default layout algorithm will give the correct result on all systems, there is no need to label the type.

4. Marshaling list for all types

The following list defines all of the primitive C# types and their counterparts for default marshaling, and marshaling when "NativeTypeSizes(true)" is specified.

C# Type Default Native
bool int8 native int
sbyte int8 int8
byte uint8 uint8
short int16 native short
ushort uint16 native unsigned short
char uint16 native wchar_t
int int32 native int
uint uint32 native unsigned int
long int64 native long
ulong uint64 native unsigned long
float float32 native float
double float64 native double
System.Object Object handle Object handle
System.String See below See below
System.Enum Underlying type Underlying type
System.ValueType Struct passed by value Struct passed by value

Objects of type System.String are handled differently depending upon the kind of parameter or return value:

Return values may have a MallocStringReturn attribute if the string was malloc'ed by the function and the value should be free'd automatically once it has been converted into System.String. For example:
[DllImport("libc"), return:MallocStringReturn(true)]
extern string get_current_dir_name();
When an explicit MarshalAs attribute is supplied, the values are marshaled as follows:

UnmanagedType NativeType
Bool native int
VariantBool native char
I1 int8
U1 uint8
I2 int16
U2 uint16
I4 int32
U4 uint32
I8 int64
U8 uint64
SysInt native int
SysUInt native unsigned int
R4 float32
R8 float64
RPrecise native long double
LPStr const native char *
LPWStr const uint16 *
LPTStr const native wchar_t *
Interface Runtime Object Handle
Struct Structure ValueType passed by value
LPStruct Structure ValueType passed by reference
LPVoid native void *

Note: we are using LPTStr slightly differently than in Windows. Windows uses it to select between either Ansi or Unicode variants of a function. We are using it to convert strings into arrays of native wchar_t elements, where the size of native wchar_t is system-specific.

String conversions use the currently prevailing locale to convert the Unicode System.String values into native char arrays.

Because most Unix API's take native char * strings as parameters, we always marshal strings in that manner unless explicitly overridden using LPWStr or LPTStr. For example:

[DllImport("libc"), NativeTypeSizes(true)]
extern uint wcslen([MarshalAs(LPTStr)] string str);
This converts into a call to the following C function:
extern unsigned int wcslen(const wchar_t *str);
If a System.String object is marshaled as Interface, then the object handle is passed directly. This should only be used with functions that are aware of the runtime engine's mechanism for representing strings.

We also handle Bool and VariantBool slightly differently than in Windows. The most common boolean types in Unix API's are native int and native char, with the former more common. Windows use 0 and -1 as boolean values with VariantBool. This usage is extremely rare in Unix, and can be handled using the integer types if necessary.

5. ANSI C Types

ANSI C defines a number of standard types such as size_t, and time_t. While there are common definitions for these types (e.g. unsigned int and long), it is possible that they may have unusual sizes on some systems.

There really isn't any good way to handle unusual type sizes cleanly. Two possibilities come to mind: one using custom marshalers and the other using attributes. For example:

[DllImport("libc"), NativeTypeSizes(true),
return:MarshalAs(UnmanagedType.CustomMarshaler, MarshalType="size_t")]
extern uint getpagesize();

[DllImport("libc"), NativeTypeSizes(true), return:AnsiCType("size_t")]
extern uint getpagesize();

The second is probably a little better: if the runtime engine does not understand the attribute, it will fall back to the default handling. In the example above, the default will "just work" on the majority of platforms.

Both methods require the runtime engine to have a list of standard ANSI C types. If this is extended to include other standard Unix types, the list could become quite large. A large list increases the chance that different engines will have different lists, and hence will not interoperate successfully.

More investigation is required.

6. InternalCall methods

The core runtime libraries (especially mscorlib) contain a large number of InternalCall methods. These are similar to PInvoke methods, except that the destination of the call is implicitly supplied by the runtime engine.

InternalCall methods typically operate closer to the runtime engine than PInvoke methods, and so their marshaling characteristics are a little different.

Objects of type System.String are passed as object handles to InternalCall methods unless explicitly overridden by a MarshalAs attribute. i.e. they are not automatically converted into native char arrays.

All InternalCall methods have an extra parameter inserted at the front of their parameter list to hold a handle to the currently executing thread. For example, consider the following method within System.Environment:

[MethodImpl(MethodImplOptions.InternalCall)]
extern private Object nativeCaptureStackTrace(int skip);
This obtains an object that represents a stack trace within the currently executing thread. The C counterpart to this would be declared as follows:
ObjHandle nativeCaptureStackTrace(ThreadHandle thread, int32 skip);
where ObjHandle represents the type used by the runtime engine to represent an object handle (probably a pointer of some kind), and ThreadHandle represents the type used by the runtime engine to represent a thread handle (probably also a pointer).

The underlying implementation can use thread to look up the internal runtime data structures to obtain the requested information.

The approach of adding an extra parameter to InternalCall methods avoids the use of global variables to obtain a link back to the runtime engine. PInvoke methods do not have this extra parameter because they typically refer to functions outside of the runtime engine itself.

We do not define the specifics of object handles and thread handles further in this document. Some future document may define a standard "Dotnet Native Interface" for use by InternalCall methods to interact with the runtime engine.

7. Revision History

18 July 2001: Original version
27 July 2001: