Проверка того, что в C# и .NET работает, хотя в документации этого нет. Четыре ключевых слова, замена типа у живого объекта, объект вместо условия и разбор одного ходячего заблуждения.
Замеров в проекте нет: всё, что он проверяет, либо работает, либо не работает, и числа тут ничего не добавят. Вместо замеров — отчёты и сверка с кодом возврата.
Статья: «А чё, так можно было? Недокументированный C#»
| Отчёт | Что смотрим |
|---|---|
keywords |
__makeref, __reftype, __refvalue — тип и значение переменной без GetType |
arglist |
__arglist — метод с переменным числом аргументов, который выполняется не на всякой системе |
swap |
замена указателя на таблицу методов: массив отвечает, что он строка |
boolops |
перегрузка операторов true и false: объект стоит в условии |
nullcall |
вызов метода у пустой ссылки: что падает, а что нет |
Четыре машины x64 под Windows, .NET 8, 9 и 10, сборки от 8.0.11 до 10.0.12.
Выгрузки лежат в Results: Comp_1 — Intel Core i9-10900KF, Comp_2 —
AMD Ryzen 9 5950X, Comp_3 — Intel Xeon W-2255, Comp_4 — Intel Xeon
Silver 4314.
Результат одинаков на всех машинах и всех рантаймах. Отдельно снят прогон
на Ubuntu 24.04 x64 — он отличается только отчётом arglist.
Тип переменной без вызова GetType
int __reftype даёт Int32
string __reftype даёт String
DateTime __reftype даёт DateTime
Разница с GetType
число до записи: 42
после __refvalue: 100
TypedReference.ToObject: Секрет
На Windows:
Система: Microsoft Windows 10.0.17763, X64
вызов прошёл: Int32 = 1; String = два; Double = 3,5;
На Linux та же сборка:
Система: Ubuntu 24.04.4 LTS, X64
вызов не прошёл
тип исключения: System.InvalidProgramException
сообщение: Vararg calling convention not supported.
Причина — проверка в джите: соглашение вызова с переменным числом аргументов поддерживается только на Windows и только не на 32-битном Arm.
До замены
массив отвечает: System.Int32[]
строка отвечает: System.String
После замены
массив отвечает: System.String
содержимое как строка: ..o (длина 3)
После возврата
массив отвечает: System.Int32[]
первый элемент: 111
сборка мусора прошла, куча цела
if (yes) сработал
if (no) не сработал
while с объектом оборотов 3
тернарный оператор ветка да
yes && no нет
yes || no да
ссылка пустая: True
метод обращается к полю NullReferenceException
метод к полям не обращается NullReferenceException
метод расширения выполнился, аргумент пустой
Нужны SDK .NET 8, 9 и 10.
dotnet --list-sdks
Весь прогон одной командой:
all.bat
Вручную, без скрипта:
dotnet run -c Release -f net10.0 -- checks
dotnet run -c Release -f net10.0 -- keywords
dotnet run -c Release -f net10.0 -- arglist
dotnet run -c Release -f net10.0 -- swap
dotnet run -c Release -f net10.0 -- boolops
dotnet run -c Release -f net10.0 -- nullcall
Скрытые ключевые слова компилятор принимает, а документация о них
не упоминает. __makeref создаёт TypedReference — пару из указателя
на данные и указателя на тип. __reftype возвращает тип из этой пары,
__refvalue читает и пишет значение прямо в исходную переменную.
TypedReference — это ref struct, и отсюда все запреты: его нельзя
сохранить в поле, положить в массив или передать в асинхронный метод.
__arglist объявляет метод с переменным числом аргументов без params
и без массива. Вызов вынесен в отдельный тип и помечен
[MethodImpl(MethodImplOptions.NoInlining)]: джит компилирует метод целиком,
поэтому при встроенном вызове он откажется от всего метода, куда тот попал,
и отчёт упал бы, не выведя ни строки.
Выполняется такой вызов не везде, поэтому сверка не требует одного исхода, а проверяет, что исход совпал с ожидаемым для этой системы. Каждый отчёт выводит систему и архитектуру.
Замена указателя на таблицу методов трогает живой объект в куче. Оставлять объект в таком виде нельзя: сборщик мусора прочитает его поля по описанию чужого типа и уронит процесс. Поэтому замена и возврат идут в одном методе, между ними нет ни выхода, ни вызова сборки мусора, а сразу после возврата отчёт вызывает сборку и убеждается, что процесс не упал.
Содержимое объекта с заменённой таблицей выводится с заменой непечатаемых символов: там необработанная память, и показать её как есть нельзя.
Операторы true и false перегружаются только парой. Вместе с оператором &
они дают короткий && для своего типа: & считает результат, а true и
false решают, нужно ли вообще вычислять правый операнд.
Вызов метода у пустой ссылки проверяется на трёх случаях: метод с обращением к полю, метод без обращения и метод расширения.
| Что проверялось | Чем |
|---|---|
Скрытые слова дают тот же тип, что GetType |
отчёт checks сравнивает __reftype с typeof |
__refvalue пишет в исходную переменную |
отчёт checks читает переменную после записи |
Исход вызова с __arglist совпал с ожидаемым для системы |
отчёт checks сравнивает результат с проверкой из target.h |
| Замена типа обратима и не роняет кучу | отчёт checks возвращает указатель и вызывает сборку мусора |
| Объект работает как условие | отчёт checks проверяет обе ветки |
| Обычный метод у пустой ссылки падает | отчёт checks проверяет метод с полем и без поля |
| Поведение не зависит от сборщика мусора | режим DOTNET_gcServer=1 для замены типа |
| Поведение не зависит от многоуровневой компиляции | режим DOTNET_TieredCompilation=0 для __arglist |
| Поведение не зависит от версии | .NET 8, 9 и 10 |
| Поведение зависит от системы | отчёты выводят систему и архитектуру, __arglist работает только на Windows |
Сверка возвращает код 1 при расхождении, но прогон на этом не останавливается: отчёты нужны в любом случае. Батник в конце сообщает, что сверка не прошла, и указывает, в каком файле смотреть.
Все проверки сняты на x64. Замена таблицы методов держится на том, что указатель на неё записан в начале объекта, — на другой разрядности смещения другие.
Results\Comp_N\
checks_netN.0.txt сверка, код возврата
keywords_netN.0.txt скрытые ключевые слова
arglist_netN.0.txt метод с переменным числом аргументов
swap_netN.0.txt замена типа у живого объекта
boolops_netN.0.txt операторы true и false
nullcall_netN.0.txt вызов метода у пустой ссылки
swap_netN.0_servergc.txt замена типа на серверном сборщике
arglist_netN.0_notiered.txt то же без многоуровневой компиляции
UndocumentedProof.csproj многоцелевой: net8.0, net9.0, net10.0
UndocumentedProof.slnx
Program.cs точка входа, разбор аргументов
README.md
all.bat весь прогон одной командой
Types\
Shapes.cs класс с операторами true и false, класс для пустой ссылки
Vararg.cs метод с переменным числом аргументов
Innards.cs чтение и запись указателя на таблицу методов
Diagnostics\
Checks.cs сверка, код возврата
Keywords.cs скрытые ключевые слова
ArgList.cs переменное число аргументов
MethodTableSwap.cs замена типа
BoolOps.cs операторы true и false
NullCall.cs вызов у пустой ссылки
Results\
Comp_1 .. Comp_4\ выгрузки прогона, по папке на машину
- Проверка поддержки переменного числа аргументов в джите
- Отказ джита при разборе вызова
- Таблица методов, описанная на C#
- Заголовок объекта в исходниках рантайма
- FieldInfo.SetValueDirect и GetValueDirect
- Операторы true и false в SqlBoolean
- TypedReference
- ArgIterator
- Операторы true и false
- Методы расширения
Проект трогает внутренности рантайма и держится на том, что указатель на таблицу методов записан в начале объекта. Microsoft этого нигде не обещала: реализацию могут изменить, и тогда приём перестанет работать. Сверка это заметит.
__makeref, __reftype, __refvalue и __arglist в документации
не описаны. Их могут убрать в любой версии компилятора без предупреждения —
поэтому отчёты и запускаются на трёх рантаймах.