C++/CLI: Attributes

Rex Jaeschke describes the keyword-like descriptive declarations called attributes provided by the .NET Framework, and shows how to use them in C++/CLI.


June 06, 2007
URL:http://www.drdobbs.com/cpp/ccli-attributes/199901816

Rex Jaeschke is an independent consultant, author, and seminar leader. He serves as editor of the standards for C++/CLI, CLI, and C#. Rex can be reached at http://www.RexJaeschke.com.


The runtime environment permits keyword-like descriptive declarations, called attributes, to be present on program elements such as types, fields, functions, and properties. Attribute information is stored in the metadata output by a compiler. As such, it can be used to describe the code to the runtime, or to modify somehow behavior at run time.

In this article, we'll see some of the attributes provided by the .NET Framework, and we'll look at several in detail.

Introduction

Depending on how it was defined, an attribute can be applied to one or more program elements. The general form of an attribute is as follows:

[assembly:Attrib1( ... )]

[Attrib2( ... )]
public ref class C
{
        [Attrib3( ... )]
        member declaration
}

In C++/CLI, an attribute is specified inside a pair of matching brackets ([]), as shown above. An attribute can be attached to the assembly as a whole (as with Attrib1), to a type (as with Attrib2), or to a member of a type (as with Attrib3). It can even be attached to a particular parameter in a function declaration, or to the return type of a function (neither of which is shown here).

In previous installments, we saw the use of a number of attributes provided by the .NET Framework; they are: Flags, ThreadStatic, Serializable, and NonSerialized.

The full name of an attribute class contains the suffix Attribute; for example, the full name of the attribute ThreadStatic is really ThreadStaticAttribute; however, the suffix can be omitted, and it has been throughout this text.

When an attribute is used, a call to its constructor is placed within the brackets. To see what arguments can be passed to that constructor, consult the on-line help for the given attribute. Apart from having one or more fixed arguments, some arguments can be passed by name rather than by position. For example,

[DllImport("MyLib.DLL", EntryPoint = "MyFunction", SetLastError = true,
CharSet = CharSet::Unicode, ExactSpelling = true,
CallingConvention = CallingConvention::StdCall)]

In such cases, the ordering of the named arguments is irrelevant.

Predefined .NET Attributes

Many attributes are provided with the .NET implementation; however, not all of them are included in the CLI Standard. They are all derived directly from System::Attribute, which, in turn, is derived directly from System::Object. Some examples are:

System::CLSCompliantAttribute
System::Diagnostics::ConditionalAttribute
System::FlagsAttribute

System::NonSerializedAttribute
System::ObsoleteAttribute
System::ParamArrayAttribute


System::Runtime::InteropServices::DllImportAttribute
System::Runtime::InteropServices::FieldOffsetAttribute
System::Runtime::InteropServices::InAttribute

System::Runtime::InteropServices::OutAttribute
System::SerializableAttribute
System::ThreadStaticAttribute

Enum Value Formatting

In a previous installment, the following reader exercise was proposed: "Many enum types are defined simply to have a set of named constants with distinct values. Others are defined with enumerators whose initial values are explicitly initialized with distinct values that are a power of two (as in 1, 2, 4, 8, etc.). Define a CLI enum type having the latter and create an instance whose value is the bitwise-or of multiple enumerators. Display the value of that instance. Then apply the attribute [Flags] to that CLI enum type and run the program again. What is the difference? Read the documentation on the type System::FlagsAttribute." The following example is a solution to a slight variation of that problem:

using namespace System;

public enum class Color {Red = 1, Blue = 2, Yellow = 4};
[Flags] public enum class Position {Left = 1, Right = 2, Top = 4, Bottom = 8};

int main()
{
/*1*/   Console::WriteLine(Color::Red);
/*2*/   Console::WriteLine(Color::Red | Color::Blue);
/*3*/   Console::WriteLine(Color::Red | Color::Blue | Color::Yellow);

/*4*/   Console::WriteLine(Position::Left);
/*5*/   Console::WriteLine(Position::Left | Position::Right);
/*6*/   Console::WriteLine(Position::Left | Position::Right | Position::Top);
}

The output produced, is as follows:

Red
3
7

Left
Left, Right
Left, Right, Top

The CLI classifies enum constants into two categories: enumeration constants and bit-fields (which are not to be confused with C++'s own bit-fields). Quoting the CLI: "Bit-fields are generally used for lists of elements that might occur in combination; whereas enumeration constants are generally used for lists of mutually exclusive elements. Therefore, bit-fields are designed to be combined with the bitwise OR operator to generate unnamed values, whereas enumerated constants are not." Using that classification, Position's members are bit-fields while Color's are not, and the presence of the [Flags] attribute on Position's definition indicates that for certain operations, such as formatted output, combinations of enum values should be handled differently.

StructLayout and FieldOffset

The first of these attributes allows the programmer to control the physical layout of the fields in a ref class or value class. There are three possible orderings, Auto, Explicit, and Sequential, each of which is specified by an enumeration value of the same name, from the enumeration type LayoutKind. Auto layout results in the runtime's choosing an appropriate layout; this is the default. Explicit layout allows the programmer to dictate the precise position of each field by using a FieldOffset attribute. With sequential layout, the fields are laid out sequentially, in the order in which they are declared, with some suitable packing between adjacent fields. Here's an example of explicit layout control, and the output it produces:

using namespace System;
using namespace System::Runtime::InteropServices;



[StructLayout(LayoutKind::Explicit)]
ref struct Overlap
{
        [FieldOffset(0)] double d;

        [FieldOffset(0)] long long int lli;

        [FieldOffset(0)] int i0;
        [FieldOffset(4)] int i1;

        [FieldOffset(0)] unsigned char b0;
        [FieldOffset(1)] unsigned char b1;
        [FieldOffset(2)] unsigned char b2;
        [FieldOffset(3)] unsigned char b3;
        [FieldOffset(4)] unsigned char b4;
        [FieldOffset(5)] unsigned char b5;
        [FieldOffset(6)] unsigned char b6;
        [FieldOffset(7)] unsigned char b7;
};



int main()
{
	Overlap o;
	o.lli = o.i0 = o.i1 = 0;
	o.b0 = o.b1 = o.b2 = o.b2 = o.b3 = o.b4 = o.b5 = o.b6 = o.b7 = 0;
	o.d = 123.456E56;

	Console::WriteLine("d:   {0}", o.d);
	Console::WriteLine("lli: {0:X16} ({1})", o.lli, o.lli);
	Console::WriteLine("i:   {0:X8} {1:X8} ({2}, {3})",
					o.i1, o.i0, o.i1, o.i0);
	Console::WriteLine("b:   {0:X2} {1:X2} {2:X2} {3:X2} {4:X2} "
					"{5:X2} {6:X2} {7:X2}",
					o.b7, o.b6, o.b5, o.b4, o.b3, o.b2, o.b1, o.b0);
}

The output produced is:

d:   1.23456E+58
lli: 4BFF77E1401420EC (5476227481232220396)
i:   4BFF77E1 401420EC (1275033569, 1075060972)
b:   4B FF 77 E1 40 14 20 EC

The attribute StructLayout is applied to the type as a whole, while each of the fields that type contains has the attribute FieldOffset. In Visual C++, the field lli exactly overlays the double d, the pair of ints i0 and i1, and the eight byte fields b0b7, resulting in a type having the characteristics of a Standard  C++ union. This is confirmed by the output produced.

Consider the following layouts (see directory At05):

[StructLayout(LayoutKind::Explicit)]
value struct SL1
{
        [FieldOffset(0)] int v;
        [FieldOffset(4)] unsigned char b;
        [FieldOffset(8)] int w;
};

In this example, the layout is explicit, with the three fields having addresses 4 bytes apart.

[StructLayout(LayoutKind::Sequential, Pack = 4)]
value struct SL2
{
        int v;
        unsigned char b;
        int w;
};

In this example, the layout is sequential, there are no explicit offsets, and the packing factor is 4. This also results in the three fields having addresses 4 bytes apart.

DllImport

With the aid of the DllImport attribute, we can call unmanaged functions, provided they reside in a DLL. For example, the following C++/CLI program calls an unmanaged C function called Hypot that resides in a DLL called Tools.dll:

using namespace System;
using namespace System::Runtime::InteropServices;

[DllImport("Tools.dll", CallingConvention =
CallingConvention::StdCall)]
extern double Hypot(double side1, double side2);

The first argument one must pass to the DllImport constructor is the DLL's name, as shown. This attribute has a number of fields, whose values can be set during construction. The one used here is CallingConvention, which has been set to the value StdCall. (This also happens to be the default calling convention.)

int main()
{
        Console::WriteLine("Hypotenuse = {0}", Hypot(3, 4));
        Console::WriteLine("Hypotenuse = {0}", Hypot(5, 12));
        Console::WriteLine("Hypotenuse = {0}", Hypot(2.34, 6.78));
}

The output produced is:

Hypotenuse = 5
Hypotenuse = 13
Hypotenuse = 7.17244728108893

Here then is the unmanaged C source:

#include <math.h>
__declspec(dllexport) double __stdcall Hypot(double side1, double side2)
{
        return sqrt((side1 * side1) + (side2 * side2));
}

Since the default calling convention for C is cdecl, the keyword stdcall has been used. (Alternatively, the default could have been used and the calling convention changed in the C++/CLI code.)

Here are some more examples; these involve calling functions in the Win32 API library and the Visual C++ Standard C library:

using namespace System;
using namespace System::Text;
using namespace System::Runtime::InteropServices;

// BOOL SetCurrentDirectory(LPCTSTR pstrDirName);
[DllImport("Kernel32.dll", CharSet = CharSet::Unicode)]
extern bool SetCurrentDirectory(String^ pstrDirName);

// DWORD GetCurrentDirectory(DWORD dwLen, LPTSTR pstrDirName);
[DllImport("Kernel32.dll", CharSet = CharSet::Unicode)]
extern int GetCurrentDirectory(int length, StringBuilder^ pstrDirName);

Functions that expect a C-style string (that is, a null-terminated array of char or wchar_t), and that do not modify that string, have their corresponding argument declared as String^. If the Unicode (16-bit character) version is wanted, that is the character set selected; if the ANSI (8-bit character) version is wanted, that is selected.

Functions that expect a C-style string and that do modify that string have their corresponding argument declared as StringBuilder^.

Since all the functions in the Win32 API library use the StdCall calling convention, and that is the default for the DllImport attribute, the calling convention need not be stated.

// UINT GetSystemDirectory(LPTSTR lpBuffer, UINT uSize)
[DllImport("Kernel32.dll", CharSet = CharSet::Auto)]
extern unsigned int GetSystemDirectory(StringBuilder^ sysDirBuffer,
        unsigned int size);

// BOOL GetUserName(LPTSTR lpBuffer, LPDWORD nSize);
[DllImport("Advapi32.dll", CharSet = CharSet::Auto)]
extern bool GetUserName(StringBuilder^ userNameBuffer,
        unsigned int *size);

// int MessageBox(HWND hWnd, LPCTSTR
lpText, LPCTSTR lpCaption,
        UINT uType)
[DllImport("User32.dll")]
extern int MessageBox(IntPtr hWnd, String^ text, String^ caption,
        unsigned int type);

The managed type IntPtr is used to hold a generic address.

// int printf(const char *format [, argument]... );
[DllImport("msvcrt.dll", CharSet = CharSet::Ansi,
        CallingConvention = CallingConvention::Cdecl)]
extern int printf(String^ format, int i);

[DllImport("msvcrt.dll", CharSet = CharSet::Ansi,
        CallingConvention = CallingConvention::Cdecl)]
extern int printf(String^ format, double d);

Functions, like printf, having a variable number of arguments, can be imported via a number of overloads.

Since all the functions in the Standard C library use the Cdecl calling convention, and that is not the default for the DllImport attribute, the calling convention is specified.

// int wprintf(const wchar_t *format [, argument]... );
[DllImport("msvcrt.dll", CharSet = CharSet::Unicode,
        CallingConvention = CallingConvention::Cdecl)]
extern int wprintf(String^ format, int i);

[DllImport("msvcrt.dll", CharSet = CharSet::Unicode,
        CallingConvention = CallingConvention::Cdecl)]
extern int wprintf(String^ format, double d);

As defined, printf is intended to deal with single-byte characters, while wprintf is intended for wide characters; that is, Unicode.

// size_t strlen(const char *str);
[DllImport("msvcrt.dll", CharSet = CharSet::Ansi,
        CallingConvention = CallingConvention::Cdecl)]
extern unsigned int strlen(String^ str);



int main()
{
	StringBuilder^ curDirBuffer = gcnew StringBuilder(256);
	int result = GetCurrentDirectory(curDirBuffer->Capacity,
					curDirBuffer);
	Console::WriteLine("Current path is >{0}<",
curDirBuffer);

	StringBuilder^ sysDirBuffer = gcnew StringBuilder(256);
	GetSystemDirectory(sysDirBuffer,
					(unsigned int)sysDirBuffer->Capacity);
	Console::WriteLine("The system directory is >{0}<",
sysDirBuffer);

	StringBuilder^ userNameBuffer = gcnew StringBuilder(128);
	unsigned int size = (unsigned int)userNameBuffer->Capacity;
	GetUserName(userNameBuffer, &size);
	Console::WriteLine("The username is >{0}<",
userNameBuffer);

	result = MessageBox(IntPtr::Zero, "MyMessage",
"MyTitle", 1);"
	Console::WriteLine("Call result: {0}", result.ToString() );

	SetCurrentDirectory("e:\\temp");
	result = GetCurrentDirectory(curDirBuffer->Capacity, curDirBuffer);
	Console::WriteLine("Current path is >{0}<",
curDirBuffer);

	printf("printf: %d\n", 123);
	printf("printf: %f\n", 98.76);
	wprintf("wprintf: %d\n", 123);
	wprintf("wprintf: %f\n", 98.76);

	Console::WriteLine("String length = {0}",
					strlen("Managed C++").ToString());
}

CLSCompliant

The Common Language Specification (CLS) is a set of rules for writing managed components that can be accessed by any CLI/.NET-based language. Not all languages support the same constructs. For example, most do not have the notion of pointers, nor do they support unsigned integer types. In addition, not all languages are case-sensitive.

We can indicate whether a program element is CLS-compliant by using an attribute; for example:

using namespace System;

[CLSCompliant(true)]
public ref class Point sealed
{
        // ...
};

The attribute CLSCompliant is false by default. It can be attached to any class member. One can declare a whole class to be CLS-compliant, and then declare one or more of its elements to be non-CLS-compliant. CLS-compliance information can be used by tools that process metadata.

Obsolete

As components evolve over time, it is possible that some of the facilities they export can become superseded. In such cases, we can declare them obsolescent, as follows:

using namespace System;

public ref class Calendar
{
        // ...
public:
        [Obsolete("GetMonth is an archaic name; use GetMonthName
instead")]
        String^ GetMonth(int number) { return ""; /* fake it for now
*/ }
        String^ GetMonthName(int number) { return ""; /* fake it for
now */ }
};

As we can see in class Calendar, the old function, GetMonth, has been superseded by GetMonthName. The string provided in the Obsolete attribute can be arbitrary.

using namespace System;

int main()
{
        Calendar^ c = gcnew Calendar;
        String^ str = c->GetMonth(4);
}

Note that the class itself is not obsolete, so we can create variables of that type without any warning. However, when we call the obsolete function, the compiler produces the following warning:

warning C4947: 'Calendar::GetMonth' : marked as obsolete
Message: 'GetMonth is an archaic name; use GetMonthName instead'

In some language modes (such as C# and Visual Basic .NET, but not C++ or J#), the source text involving calls to obsolete functions is highlighted, and hovering over it results in a ToolTip containing the attribute's text.

Custom Attributes

We can create our own attribute classes, by deriving them from class Attribute. In doing so, we can define the program elements to which a custom attribute can be attached. Of course, compilers and runtime library code will not be looking for our custom attributes; we'd have to have our own tools to locate and process them. Custom attributes are not discussed further here.

Exercises

To reinforce the material we've covered, perform the following activities:

1. Find out all the fields for the StructLayout attribute that can be given values.

1. Find out all the values for CallingConvention. What are all the fields for this attribute that can be given values?

2. Find out more information about the managed type IntPtr.

3. Hypot is written in C. What changes would we need to make if this function were written in unmanaged C++ instead? The issue here is that, by default, C++ external names are mangled. We can solve this in two ways. If we are able to change the C++ source, we could disable the name mangling; however, this approach is not possible if other programs are currently using that DLL. The alternate approach requires the calling program to use the actual mangled name. Hint: use the DUMPBIN utility from the command line to inspect the DLL to find Hypot's mangled name. Then read about the EntryPoint field on the DllImport attribute.

Notes and Resources

  1. CLI stands for "Common Language Infrastructure", the subset of .NET that was standardized by Ecma Technical Committee TC39/TG3, and adopted by ISO/IEC. .NET is the name of a Microsoft product that is a superset of the CLI standard. Another implementation of the CLI is Mono, from Novell/Ximian, which runs on Windows and Linux. See http://www.mono-project.com/about/index.html.
  2. Microsoft's Visual C++ .NET is an implementation of C++/CLI. A free copy of the Express Edition of this product can be downloaded from http://www.microsoft.com/visualc.
  3. A free copy of the C++/CLI Standard can be downloaded from http://www.ecma-international.org/publications/standards/Ecma-372.htm

For more information on C++/CLI see these other articles by Rex Jaeschke:

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.