Поведение делегата CIL с конфликтующей статичностью целевого метода

Этот вопрос потребует небольшого введения.

Я работаю над проектом безопасности, который будет анализировать сборки CIL и отклонять те, которые делают определенные определенные «плохие» вещи, а также позволять хост-приложению предоставлять «ворота» для некоторых методов, чтобы разрешить фильтрацию некоторых вызовов. (Это небольшое подмножество функциональности проекта, но это та часть, о которой я буду здесь расспрашивать.)

Проект сканирует все инструкции в каждом методе сборки и ищет коды операций call, callvirt, ldftn, ldvirtftn и newobj, поскольку это единственные коды операций, которые в конечном итоге могут привести к вызову метода. Коды операций ldftn используются при создании делегатов, например:

ldarg.1
ldftn instance bool string::EndsWith(string)
newobj instance void class [System.Core]System.Func`2<string, bool>::'.ctor'(object, native int)

В конце этой последовательности Func<string, bool> находится на вершине стека.

Допустим, я хочу перехватывать все звонки на String.EndsWith(String). Для call и callvirt я могу просто заменить вызов экземпляра статическим вызовом сигнатуры Boolean(String,String) -- первым аргументом будет строковый экземпляр, для которого изначально был вызван метод. На уровне CIL поведение будет однозначным и четко определенным, поскольку именно так вызываются статические методы.

А для ldftn? Я попытался просто заменить операнд инструкции ldftn тем же статическим методом, который использовался для замены операнда call/callvirt:

ldarg.1
ldftn bool class Prototype.Program::EndsWithGate(string, string)
newobj instance void class [System.Core]System.Func`2<string, bool>::'.ctor'(object, native int)

Я полностью ожидал, что это не удастся, поскольку делегату предоставляется целевой объект (не нулевой) при передаче указателя статического метода. К моему удивлению, это действительно работает как в среде выполнения Microsoft .NET, так и в Mono. Я понимаю, что параметр target/this является лишь первым параметром метода и скрыт для методов экземпляра. (Проект основан на этих знаниях.) Но тот факт, что делегаты действительно работают в таких условиях, меня немного озадачивает.

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


person cdhowie    schedule 12.12.2010    source источник
comment
Без этого поведения вы не могли бы создать делегата из методов расширения, как из метода экземпляра.   -  person CodesInChaos    schedule 13.12.2010


Ответы (3)


В спецификации ECMA 335, часть 2 14.6.2, есть параграф об этом: Соглашение о вызовах T и D должно точно совпадать, игнорируя различие между статическими методами и методами экземпляра. (т. е. этот параметр, если он есть, специально не обрабатывается).

Как мне это звучит, для статических методов будет разрешено в двух вариантах:

  • Без этого, в этом случае NULL должен быть передан
  • С дополнительным первым параметром, предполагая, что тип соответствует тому, что было передано в вызов newobj.
person Carlo Kok    schedule 12.12.2010

Стоит отметить, что это не злоупотребление. Это метод, известный как «каррирование делегатов». Это происходит от более общей техники, называемой «каррирование» в функциональных языках программирования, где функция с аргументами N+1 преобразуется в функцию с аргументами N. Эквивалент C# будет выглядеть примерно так:

Func<T2, R> CurryFirst<T1, T2, R>(
    Func<T1, T2, R> f,
    T1 arg
)
{
    return (x) => f(arg, x);
}

Func<T1, R> CurrySecond<T1, T2, R>(
    Func<T1, T2, R> f,
    T2 arg
)
{
    return (x) => f(x, arg);
}

CLR предоставляет специальную поддержку для случая "сначала каррировать", главным образом потому, что на уровне машинного кода вызов статического метода с каррированием выглядит почти точно так же, как вызов метода экземпляра (параметр this передается в качестве неявного первого аргумента).

Это делает реализацию каррирования делегатов достаточно эффективной. Первоначально он был реализован вместе с DynamicMethod для поддержки Iron Python. Он также используется для других целей, таких как разрешение делегатам прозрачно ссылаться на методы расширения.

person Scott Wisniewski    schedule 24.12.2010
comment
Действительно ли эта функция реализована только вместе с DLR? - person Ark-kun; 10.01.2014
comment
Я считаю, что он был реализован для поддержки железного питона, который предшествует DLR как минимум на 2 года или около того. - person Scott Wisniewski; 10.01.2014

Ну, я не думал, что сам буду отвечать на свой первый вопрос...

Коллега в #mono (Ck) сообщил мне о соответствующем поведении Delegate.CreateDelegate: (выделено мной)

Если указан firstArgument, он передается методу каждый раз при вызове делегата; Говорят, что firstArgument привязан к делегату, а делегат закрыт по своему первому аргументу. Если метод статический (общий в Visual Basic), список аргументов, предоставляемый при вызове делегата, включает все параметры, кроме первого; если метод является методом экземпляра, то firstArgument передается скрытому параметру экземпляра (представленному this в C# или Me в Visual Basic).

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

person cdhowie    schedule 12.12.2010