P/Invoke

93

Механизм Platform Invoke, более известный как P/Invoke, позволяет вызывать из управляемого кода функции в стиле языка C, экспортируемые библиотеками DLL. Чтобы воспользоваться механизмом P/Invoke, управляемый код должен объявить статический внешний (static extern) метод с сигнатурой (типы параметров и тип возвращаемого значения), эквивалентной функции на языке C. Сам метод должен быть снабжен атрибутом DllImport, определяющим, как минимум, библиотеку DLL, экспортирующую требуемую функцию.

// Фактическое объявление в WinBase.h:
HMODULE WINAPI LoadLibraryW(LPCWSTR lpLibFileName);
// Объявление на C#
class MyInteropFunctions
{
    [DllImport("kernel32.dll", SetLastError = true)]
    public static extern IntPtr LoadLibrary(string fileName);
}

В предыдущем фрагменте метод LoadLibrary определяется как функция, принимающая значение типа string и возвращающая значение типа IntPtr - указатель, который не может быть разыменован непосредственно, то есть, он не делает код небезопасным. Атрибут DllImport указывает, что функция экспортируется библиотекой kernel32.dll (главная библиотека Win32 API) и что последняя ошибка Win32 должна сохраняться в локальной памяти потока, чтобы она не была затерта неявными вызовами функций Win32 (например, выполняемыми средой выполнения CLR). В атрибуте DllImport можно также указать соглашение о вызове функции, кодировку для строк, параметры разрешения экспортируемых имен, и так далее.

Если сигнатура библиотечной функции содержит составные типы, такие как структуры языка C, в управляемом коде необходимо определить эквивалентные структуры или классы, используя эквивалентные типы для каждого поля. Порядок следования полей, типы полей и применяемые к ним правила выравнивания должны совпадать с ожидаемыми в коде на языке C. В некоторых случаях может потребоваться применить атрибут MarshalAs к полям в параметрах функции или в возвращаемом значении, чтобы изменить поведение механизма маршалинга по умолчанию.

Например, управляемый тип System.Boolean (bool) может иметь разные представления в низкоуровневом коде: тип Win32 bool имеет размер четыре байта, а значению true соответствует любое ненулевое значение, тогда как в C++ значение типа bool занимает один байт, а значению true соответствует целое число 1.

В следующем фрагменте демонстрируется применение атрибута StructLayout к структуре WIN32_FIND_DATA, определяющего расположение полей в памяти; без этого среда выполнения CLR может переупорядочить поля для большей эффективности. Атрибут MarshalAs применяется к полям cFileName и cAlternativeFileName, чтобы указать, что строки должны передаваться как строки фиксированного размера, встроенные в структуру, а не как простые указатели на внешние строки:

// Фактическое объявление в WinBase.h:
typedef struct _WIN32_FIND_DATAW 
{
    DWORD dwFileAttributes;
    FILETIME ftCreationTime;
    FILETIME ftLastAccessTime;
    FILETIME ftLastWriteTime;
    DWORD nFileSizeHigh;
    DWORD nFileSizeLow;
    DWORD dwReserved0;
    DWORD dwReserved1;
    WCHAR cFileName[MAX_PATH];
    WCHAR cAlternateFileName[14];
} 
WIN32_FIND_DATAW;

HANDLE WINAPI FindFirstFileW(__in LPCWSTR lpFileName,
    __out LPWIN32_FIND_DATAW lpFindFileData);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
struct WIN32_FIND_DATA
{
    public uint dwFileAttributes;
    public FILETIME ftCreationTime;
    public FILETIME ftLastAccessTime;
    public FILETIME ftLastWriteTime;
    public uint nFileSizeHigh;
    public uint nFileSizeLow;
    public uint dwReserved0;
    public uint dwReserved1;

    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
    public string cFileName;

    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
    public string cAlternateFileName;
}

[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
static extern IntPtr FindFirstFile(string lpFileName, out WIN32_FIND_DATA lpFindFileData);

Когда программа вызовет метод FindFirstFile из предыдущего примера, среда выполнения CLR загрузит библиотеку DLL (kernel32.dll), найдет в ней требуемую функцию (FindFirstFile) и выполнит преобразование параметров из управляемого представления в низкоуровневое (и обратно). В данном примере, входной строковый параметр lpFileName будет преобразован в низкоуровневую строку, а низкоуровневую структуру win32_ find_dataw, на которую ссылается параметр lpFindFileData, - в управляемую структуру WIN32_FIND_DATAW. Все эти этапы более подробно будут описаны в следующих разделах.

PInvoke.net и P/Invoke Interop Assistant

Создание сигнатур для механизма P/Invoke может быть сложным и утомительным делом. Существует множество правил и тонкостей, которые следует помнить. Неверное определение сигнатуры может привести к появлению сложных в диагностике ошибок. К счастью, имеются два ресурса, которые могут упростить решение этой проблемы: веб-сайт PInvoke.net и утилита P/Invoke Interop Assistant.

PInvoke.net - очень полезный веб-сайт в стиле Wiki, где можно найти или передать свои сигнатуры P/Invoke для различных функций Microsoft API. Сайт PInvoke.net был создан Адамом Натаном (Adam Nathan), ведущим инженером-программистом в Microsoft, прежде работавшим в подразделении .NET CLR Quality Assurance и написавшим объемную книгу о взаимодействии с COM-объектами. Получить доступ к сигнатурам можно также с помощью расширения для Visual Studio, распространяемого бесплатно.

P/Invoke Interop Assistant - это бесплатный инструмент, который можно получить на сайте CodePlex вместе с исходными кодами. Он содержит базу данных (XML-файл) с описаниями функций Win32, структур и констант, используемых для создания сигнатур P/Invoke. Он также способен генерировать сигнатуры P/Invoke по объявлениям функций на языке C, а также создавать объявления низкоуровневых функций обратного вызова и сигнатуры низкоуровневых интерфейсов COM-объектов для заданной управляемой сборки (assembly).

На рисунке ниже показано, как выглядит окно утилиты P/Invoke Interop Assistant с результатами поиска функции «CreateFile» слева, и сигнатурой P/Invoke, вместе с необходимыми структурами - справа:

Окно утилиты P/Invoke Interop Asssitant, демонстрирующей сигнатуру P/Invoke для CreateFile

Привязка

При первом вызове функции с помощью механизма P/Invoke, обращением к функции LoadLibrary производится загрузка библиотеки DLL и всех ее зависимостей (если они еще не были загружены) в адресное пространство процесса. Затем выполняется поиск требуемой экспортируемой функции, при этом сначала предпринимается попытка найти управляемую версию функции. Порядок поиска зависит от значений полей CharSet и ExactSpelling в атрибуте DllImport:

По умолчанию поле ExactSpelling имеет значение false в C# и true в VB.NET. Во всех современных версиях Windows (выпущенных после Windows ME), значение CharSet.Auto соответствует значению CharSet.Unicode.

Используйте версии функций Win32 API для работы с Юникодом. Версии Windows NT и выше имеют встроенную поддержку Юникода (UTF16). Если пользоваться ANSI-версиями функций Win32 API, строки автоматически будут преобразовываться в Юникод и передаваться версиям функций для работы с Юникодом, что повлечет снижение производительности. Для внутреннего представления строк в .NET используется кодировка UTF16, поэтому маршалинг строковых параметров будет выполняться быстрее, если они уже будут в кодировке UTF16. Предусматривайте в своем коде и особенно в интерфейсах совместимость с Юникодом, что даст вам дополнительные преимущества с точки зрения глобализации. Устанавливайте ExactSpelling в значение true, это повысит производительность на начальном этапе за счет устранения поиска ненужных функций.

Заглушки маршалера

При первом вызове функции с помощью механизма P/Invoke, сразу после загрузки библиотеки DLL, заглушки (stubs) маршалера P/Invoke генерируются по требованию, и повторно используются в последующих вызовах. При вызове маршалер выполняет следующие действия:

  1. Проверяет наличие у вызывающего процесса права на выполнение неуправляемого кода.

  2. Преобразует управляемые аргументы в их неуправляемые аналоги, возможно с выделением памяти.

  3. Устанавливает вытесняющий режим работы для потока выполнения сборщика мусора, чтобы сборка мусора могла производиться, не дожидаясь, пока поток выполнения достигнет безопасной точки.

  4. Вызывает библиотечную функцию.

  5. Восстанавливает кооперативный режим работы потока выполнения сборщика мусора.

  6. При необходимости сохраняет код ошибки Win32 в локальной переменной потока для последующего доступа с помощью метода Marshal.GetLastWin32Error().

  7. При необходимости преобразует значение типа HRESULT в исключение и возбуждает его.

  8. Преобразует низкоуровневое исключение, если оно было возбуждено, в управляемое исключение.

  9. Преобразует возвращаемое значение и выходные параметры обратно в их управляемые аналоги.

  10. Освобождает любые временные блоки динамической памяти, выделенные при вызове.

Механизм P/Invoke может также использоваться низкоуровневым кодом для вызова управляемых методов. Обратная заглушка маршалера может быть сгенерирована для делегата (через Marshal.GetFunctionPointerForDelegate), если передается механизму P/Invoke как параметр неуправляемой функцией. В ответ неуправляемая функция получит указатель на функцию, которую можно вызвать, чтобы обратиться к управляемому методу. Этот указатель на функцию будет ссылаться на заглушку, которая помимо параметров маршалинга знает также адрес целевого объекта (указатель this).

В версии .NET Framework 1.x заглушки маршалера представляли собой либо код сгенерированной сборки (для простых сигнатур), либо код на языке ML (Marshaling Language) (для сложных сигнатур). Язык ML - это внутренний байт-код, выполняемый внутренним интерпретатором.

С появлением в .NET Framework 2.0 поддержки процессоров AMD64 и Itanium, в Microsoft обнаружили, что реализация параллельной инфраструктуры ML для каждого типа процессоров - слишком обременительное занятие. Поэтому заглушки для 64-разрядных версий .NET Framework 2.0 были реализованы исключительно на языке IL. Хотя заглушки на языке IL действуют значительно быстрее заглушек на языке ML, они все равно оказываются медленнее заглушек на языке ассемблера x86, поэтому в Microsoft было решено сохранить реализацию для архитектуры x86.

В .NET Framework 4.0, инфраструктура генерирования заглушек на языке IL была значительно оптимизирована, что сделало заглушки на языке IL даже быстрее, чем заглушки на языке ассемблера x86. Это позволило Microsoft полностью удалить реализацию заглушек для аппаратной архитектуры x86 и унифицировать их для всех поддерживаемых платформ.

Вызов функции, пересекающий границу между управляемым и неуправляемым кодом, как минимум на порядок медленнее непосредственного вызова в том же окружении. Если и управляемый, и неуправляемый код находятся в вашем ведении, конструируйте интерфейсы так, чтобы уменьшить количество взаимодействий между управляемым и неуправляемым кодом. Попробуйте объединить несколько «заданий» в один вызов. Аналогично, старайтесь объединять несколько вызов простых функций (таких как функции Get/Set) в один вызов, выполняющий всю необходимую работу.

Корпорация Microsoft предоставляет бесплатный инструмент IL Stub Diagnostics, который можно получить на сайте проекта CodePlex вместе с исходными кодами. Он подписывается на события CLR ETW IL создания заглушек и отображает сгенерированный код заглушек на языке IL в графическом интерфейсе.

Ниже приводится пример заглушки маршалера на языке IL с комментариями, состоящий из пяти разделов: инициализация, маршалинг входных параметров, вызов, маршалинг обратно возвращаемого значения и/или выходных параметров, и завершение. Заглушка маршалера генерируется для следующей сигнатуры:

// Управляемая сигнатура
[DllImport("Server.dll")]static extern int Marshal_String_In(string s);
// Неуправляемая сигнатура
unmanaged int __stdcall Marshal_String_In(char *s)

В разделе инициализации заглушка объявляет локальные переменные (на стеке), получает контекст заглушки и проверяет право на выполнение неуправляемого кода:

// Заглушка IL:
// Объем кода 153 (0x0099)
.maxstack 3

// Локальные переменные:
// IsSuccessful, pNativeStrPtr, SizeInBytes, pStackAllocPtr, result, result, result
.locals (int32,native int,int32,native int,int32,int32,int32)

call native int [mscorlib] System.StubHelpers.StubHelpers::GetStubContext()

// Проверка права на выполнение неуправляемого кода
call void [mscorlib] System.StubHelpers.StubHelpers::DemandPermission(native int)

В разделе маршалинга заглушка передает входные параметры неуправляемой функции. В данном примере выполняется маршалинг единственного входного строкового параметра. Для преобразования конкретных типов и их категорий из управляемого представления в неуправляемое, и обратно, маршалер может использовать вспомогательные типы в пространстве имен System.StubHelpersnamespace или обращаться к классу System.Runtime.InteropServices.Marshal. В данном примере для маршалинга строки вызывается метод CSTRMarshaler::ConvertToNative.

Здесь используется небольшая оптимизация: если управляемая строка достаточно короткая, память для нее выделяется на стеке (что намного быстрее). В противном случае для нее выделяется блок в динамической памяти:

    ldc.i4 0x0               // IsSuccessful = 0 [положить 0 на стек]
    stloc.0                  // [сохранить в IsSuccessful]
    
IL_0010:
    nop                      // аргумент {
    ldc.i4 0x0               // pNativeStrPtr = null [положить 0 на стек]
    conv.i                   // [преобразовать int32 в "неуправляемый int" (указатель)]
    stloc.3                  // [сохранить результат в pNativeStrPtr]
    ldarg.0                  // if (managedString == null)
    brfalse     IL_0042      // goto IL_0042
    ldarg.0                  // [экземпляр managedString положить на стек]
                             // обратиться к свойству Length (получить число символов)
                             
    call instance int32 [mscorlib] System.String::get_Length()
    
    ldc.i4 0x2               // Прибавить 2 к длине, 1 - для нулевого символа
                             // в managedString, и 1 - для дополнительного нулевого символа
                             // [положить константу 2 на стек]
    add                      // [фактическое сложение, результат - на стеке]
                             // загрузить статическое поле, значение зависит от 
                             // lang. для приложений, не поддерживающих Юникод
                             
    ldsfld System.Runtime.InteropServices.Marshal::SystemMaxDBCSCharSize
    
    mul                      // Умножить длину на SystemMaxDBCSCharSize чтобы 
                             // получить количество байтов
    stloc.2                  // Сохранить в SizeInBytes
    ldc.i4 0x105             // Сравнить SizeInBytes и 0x105, чтобы исключить 
                             // возможность выделения слишком большого количества 
                             // памяти на стеке [константу 0x105 на стек]
                             // CSTRMarshaler::ConvertToNative для случая
                             // pStackAllocPtr == null и вызовет CoTaskMemAlloc
                             // для выделения большего объема памяти
    ldloc.2                  // [Положить SizeInBytes]
    clt                      // [If SizeInBytes > 0x105, положить 1 иначе 0]
    brtrue IL_0042           // [If 1 goto IL_0042]
    ldloc.2                  // Положить SizeInBytes (аргумент для localloc)
    localloc                 // Выделить память на стеке, указатель оставить на стеке
    stloc.3                  // Сохранить в pStackAllocPtr
    
IL_0042:
    ldc.i4 0x1               // константу 1 на стек (параметр flags)
    ldarg.0                  // аргумент managedString на стек
    ldloc.3                  // pStackAllocPtr на стек (this может быть null)
                             // Вызвать функцию преобразования Unicode в ANSI
    call native int [mscorlib]System.StubHelpers.
        CSTRMarshaler::ConvertToNative(int32,string, native int)
        
    stloc.1                  // Сохранить результат в pNativeStrPtr,
                             // может быть равен pStackAllocPtr
    ldc.i4 0x1               // IsSuccessful = 1 [положить 1 на стек]
    stloc.0                  // [сохранить в IsSuccessful]
    nop
    nop
    nop

В следующем разделе заглушка получает из своего контекста указатель на неуправляемую функцию и вызывает ее. Инструкция вызова в действительности делает больше, чем кажется. Например, она изменяет режим работы сборщика мусора, перехватывает значение, возвращаемое неуправляемой функцией, и приостанавливает выполнение управляемого кода, пока сборщик мусора выполняет фазу, требующую этого:

ldloc.1                  // Положить pStackAllocPtr на стек,
                         // для пользовательской функции, не для GetStubContext
                         
call native int [mscorlib] System.StubHelpers.StubHelpers::GetStubContext()

ldc.i4 0x14              // Добавить 0x14 к указателю контекста
add                      // [фактическое сложение, результат на стеке
ldind.i                  // [разыменовать указатель, результат на стеке]
ldind.i                  // [разыменовать указатель на функцию, результат на стеке]

calli unmanaged stdcall int32(native int)   // Вызвать пользовательскую функцию

Следующий раздел фактически состоит из двух разделов, выполняющих обратное преобразование неуправляемых типов в управляемые для возвращаемого значения и выходных параметров, соответственно. В данном примере, неуправляемая функция возвращает значение типа int, не требующее преобразования (маршалинга), которое просто копируется в локальную переменную в исходном виде. Поскольку здесь нет выходных параметров, последний раздел остался пустым:

// UnmarshalReturn {
    nop                // возврат {
    stloc.s 0x5        // Сохранить результат функции (int) в x, y и z
    ldloc.s 0x5
    stloc.s 0x4
    ldloc.s 0x4
    nop                // } возврат
    stloc.s 0x6
// } UnmarshalReturn
// Unmarshal {
    nop                // аргумент {
    nop                // } аргумент
    leave IL_007e      // Выход из блока try
    
IL_007e:
    ldloc.s 0x6        // Поместить z на стек
    ret                // Вернуть z
// } Unmarshal

Наконец, в разделе завершения освобождается память, выделенная временно для нужд маршалинга. Эти операции выполняются в блоке finally инструкции try, поэтому они будут выполнены, даже если неуправляемая функция возбудит исключение. Имеется также возможность выполнять некоторые действия только в случае исключения. Во взаимодействиях с COM-объектами заглушки могут преобразовывать возвращаемое значение HRESULT, указывающее на ошибку, в исключение:

// ExceptionCleanup {
IL_0081:
// } ExceptionCleanup
// Cleanup {
    ldloc.0               // If (IsSuccessful && !pStackAllocPtr)
    ldc.i4 0x0            // Вызвать ClearNative(pNativeStrPtr)
    ble IL_0098
    ldloc.3
    brtrue IL_0098
    ldloc.1
    call void [mscorlib] System.StubHelpers.CSTRMarshaler::ClearNative(native int)
    
IL_0098:
    endfinally
    
IL_0099:
// } Cleanup
    .try IL_0010 to IL_007e finally handler IL_0081 to IL_0099

Как видите, заглушка маршалера на языке IL имеет замысловатую организацию даже для функций с такой простой сигнатурой. Для сложных сигнатур генерируются еще более длинные и медленные заглушки.

Двоично совместимые типы

Многие неуправляемые типы двоично совместимы с управляемым кодом. Эти типы, называемые двоично совместимыми (blittable), не требуют преобразования и передаются через границу между управляемым и неуправляемым кодом намного быстрее, чем двоично несовместимые (non-blittable) типы. В действительности заглушка маршалера может оптимизировать такую передачу еще больше, закрепляя управляемый объект и передавая неуправляемому коду указатель на него, исключая одну или две операции копирования (по одной для каждого направления передачи).

К двоично совместимым типам относятся:

System.Byte (byte)
System.SByte (sbyte)
System.Int16 (short)
System.UInt16 (ushort)
System.Int32(int)
System.UInt32 (uint)
System.Int64 (long)
System.UInt64 (ulong)
System.IntPtr
System.UIntPtr
System.Single (float)
System.Double (double)

Кроме того, одномерные массивы с элементами двоично совместимых типов (где все элементы принадлежат одному и тому же типу) также являются двоично совместимыми, так же как структуры или классы, состоящие только из полей двоично совместимых типов.

Тип System.Boolean (bool) не является двоично совместимым, потому что в неуправляемом коде он может иметь 1, 2 или 4-байтное представление. Тип System.Char (char) не является двоично совместимым, потому что может представлять символ ANSI или Юникода. Тип System.String (string) не является двоично совместимым, потому что его неуправляемое представление может состоять из символов ANSI или Юникода, и может быть строкой в стиле языка C или COM BSTR, а также потому, что управляемая строка должна быть неизменяемой.

Тип, содержащий поле со ссылкой на объект, не может быть двоично совместимым, даже если это ссылка на двоично совместимый тип или массив. При маршалинге двоично несовместимых типов требуется выделять память для сохранения преобразованных версий параметров, ее заполнения и последующего освобождения.

Наивысшей производительности можно добиться, реализовав маршалинг входных строковых параметров вручную (см. следующий фрагмент кода). Но при этом вызываемая неуправляемая функция должна принимать строку в кодировке UTF-16, в стиле языка C, и никогда не писать в память, занимаемую строкой, из-за чего такая оптимизация редко бывает применима. Чтобы выполнить маршалинг вручную, необходимо закрепить входную строку, изменить сигнатуру P/Invoke так, чтобы неуправляемая функция выглядела, как принимающая IntPtr вместо String, и передавать ей указатель на закрепленную строку:

class Win32Interop
{
    [DllImport("NativeDLL.DLL", CallingConvention = CallingConvention.Cdecl)]

    // IntPtr вместо string
    public static extern void NativeFunc(IntPtr pStr); 
}


// ...


// Управляемый код вызывает функцию P/Invoke внутри области видимости 
// fixed, что обеспечивает закрепление строки
unsafe
{
    string str = "MyString";
    fixed (char* pStr = str)
    {
        // pStr можно использовать в нескольких вызовах
        Win32Interop.NativeFunc((IntPtr)pStr);
    }
}

Преобразование неуправляемой строки UTF-16 в стиле языка C в управляемую строку также можно оптимизировать, применив конструктор System.String, принимающий параметр типа char*. Конструктор System.String создает копию буфера, поэтому неуправляемую память, занимаемую строкой, можно освободить сразу после создания управляемой строки. Обратите внимание, что здесь не выполняется никакой проверки, чтобы убедиться, что строка содержит только допустимые символы Юникода.

Направление маршалинга, ссылочные типы и типы значений

Как упоминалось выше, параметры функции могут передаваться заглушкой маршалера в одном или в двух направлениях. Направление передачи параметра определяется рядом факторов:

Для дальнейшего обсуждения будем обозначать направление маршалинга из управляемого кода в неуправляемый как «in»; а направление из неуправляемого кода в управляемый - как «out». Ниже приводится список правил определения направления маршалинга по умолчанию:

Добавление атрибута OutAttribute запрещает маршалинг в направлении «in», поэтому вызываемый неуправляемый код может не получить значение, переданное вызывающим кодом. Ключевое слово out в языке C# действует подобно ключевому слову ref, но добавляет атрибут OutAttribute.

Если типы параметров не являются двоично совместимыми в вызове P/Invoke и вам требуется организовать маршалинг только в направлении «out», ненужного маршалинга в направлении «in» можно избежать, использовав ключевое слово out вместо ref.

Из-за закрепления параметров двоично совместимых типов при маршалинге, как описывалось выше, для двоично совместимых ссылочных типов автоматически устанавливается направление «in/out», даже если правила выше утверждают иное. Однако не следует полагаться на эту особенность, когда требуется получить поведение «out» или «in/out» маршалинга, а вместо этого указывать направление явно, с помощью атрибутов, так как данная особенность перестанет действовать, если позднее вы добавите поле двоично несовместимого типа или если это вызов COM-объекта, пересекающий границы подразделений (apartments).

Разница между маршалингом типов значений и ссылочных типов заключается в особенностях их передачи через стек:

Передача объемных типов значений (более десятка байт) по значению стоит дороже передачи их по ссылке. То же относится к объемным возвращаемым значениям, вместо которых может оказаться предпочтительнее использовать выходные параметры.

Code Access Security

Механизм .NET Code Access Security позволяет выполнять код, не вызывающий доверия, в изолированном окружении, называемом «песочницей» (sandbox), с ограниченным доступом к возможностям среды выполнения (например, P/Invoke) и BCL (например, доступ к файлам и реестру). Когда вызывается неуправляемый код, механизм Code Access Security требует, чтобы все сборки, чьи методы будут вызываться, имели право UnmanagedCode. Заглушка маршалера будет проверять это право для каждого вызова, что влечет за собой обход стека вызовов, чтобы убедиться, что весь код в цепочке вызовов обладает данным правом.

Если вы выполняете только код, пользующийся доверием, или у вас имеются иные средства, гарантирующие безопасность, вы можете значительно увеличить производительность, добавив атрибут SuppressUnmanagedCodeSecurityAttribute в объявление метода, класса (в этом случае данный атрибут применяется ко всем методам), интерфейса или делегата.

Пройди тесты
Лучший чат для C# программистов