Поддержка отражений в Си
Я знаю, что это не поддерживается, но мне интересно, есть ли какие-то хитрости вокруг этого. Какие-нибудь советы?
11 ответов
Отражение в целом - это средство для программы проанализировать структуру некоторого кода. Этот анализ используется для изменения эффективного поведения кода.
Отражение как анализ, как правило, очень слабо; обычно он может предоставлять доступ только к именам функций и полей. Эта слабость вызвана тем, что разработчики языка по сути не хотят делать полный исходный код доступным во время выполнения вместе с соответствующими процедурами анализа для извлечения того, что нужно из исходного кода.
Другой подход заключается в том, чтобы приступить к анализу программ с использованием мощного инструмента анализа программ, например, такого, который может анализировать исходный текст в точности так, как это делает компилятор. (Часто люди предлагают злоупотреблять самим компилятором, чтобы сделать это, но это обычно не работает; механизм компилятора хочет быть компилятором, и чертовски трудно согнуть его для других целей).
Необходим инструмент, который:
- Разбирает язык исходного текста
- Создает абстрактные синтаксические деревья, представляющие каждую деталь программы. (Полезно, если AST сохраняют комментарии и другие детали макета исходного кода, такие как номера столбцов, буквенные значения осей и т. Д.)
- Создает таблицы символов, показывающие объем и значение каждого идентификатора.
- Может извлекать потоки управления из функций
- Может извлекать поток данных из кода
- Можно построить граф вызовов для системы
- Можно определить, на что указывает каждый указатель
- Позволяет создавать собственные анализаторы с использованием вышеуказанных фактов
- Может преобразовать код в соответствии с такими пользовательскими анализами (обычно путем пересмотра AST, которые представляют проанализированный код)
- Может восстановить исходный текст (включая макет и комментарии) из пересмотренных AST.
Используя такой механизм, каждый реализует анализ на любом уровне детализации, а затем преобразует код для достижения эффекта, которого достигнет отражение во время выполнения. Есть несколько основных преимуществ:
- Уровень детализации или объем анализа - это вопрос амбиций (например, он не ограничен только тем, что может сделать только отражение во время выполнения)
- Нет никаких накладных расходов времени выполнения для достижения отраженного изменения в поведении
- Используемый механизм может быть общим и применяться на многих языках, а не ограничиваться тем, что обеспечивает конкретная языковая реализация.
- Это совместимо с идеей C/C++, согласно которой вы не платите за то, что не используете. Если вам не нужно отражение, вам не нужен этот механизм. И вашему языку не нужно иметь интеллектуальный багаж слабого отражения.
См. Наш инструментарий реинжиниринга программного обеспечения DMS для системы, которая может выполнять все вышеперечисленное для C, Java и COBOL, и большинство из них для C++.
Советы и хитрости всегда существуют. Взгляните на библиотеку Metaresc https://github.com/alexanderchuranov/Metaresc
Он предоставляет интерфейс для объявления типов, который также будет генерировать метаданные для типа. На основе метаданных вы можете легко сериализовать / десериализовать объекты любой сложности. Из коробки вы можете сериализовать / десериализовать XML, JSON, XDR, Lisp-подобную нотацию, нотацию C-init.
Вот простой пример:
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "metaresc.h"
TYPEDEF_STRUCT (point_t,
double x,
double y
);
int main (int argc, char * argv[])
{
point_t point = {
.x = M_PI,
.y = M_E,
};
char * str = MR_SAVE_XML (point_t, &point);
if (str)
{
printf ("%s\n", str);
free (str);
}
return (EXIT_SUCCESS);
}
Эта программа выведет
$ ./point
<?xml version="1.0"?>
<point>
<x>3.1415926535897931</x>
<y>2.7182818284590451</y>
</point>
Библиотека прекрасно работает для последних GCC и Clang.
какие-нибудь хитрости вокруг этого? Какие-нибудь советы?
Компилятор, возможно, по желанию сгенерирует "файл символов отладки", который отладчик может использовать для отладки кода. Компоновщик также может генерировать "файл карты".
Трюк / совет может быть в том, чтобы сгенерировать, а затем прочитать эти файлы.
Мне нужно было отражение в куче struct
в проекте C++.
Я создал XML-файл с описанием всех этих структур - к счастью, типы полей были примитивными.
Я использовал шаблон (не C++ template
) для автоматического создания class
для каждого struct
наряду с методами установки / получения.
В каждом class
Я использовал карту, чтобы связать имена строк и членов класса (указатели на членов).
Я не пожалел об использовании рефлексии, потому что это открыло новые способы разработки моей основной функциональности, которую я даже не мог представить без рефлексии.
(Кстати, это был внешний генератор отчетов для программы, которая использует необработанную базу данных)
Итак, я использовал генерацию кода, указатели функций и карты для имитации отражения.
Я знаю следующие варианты, но все они стоят дорого и имеют множество ограничений:
- использование
libdl
(#include <dfcln.h>
) - Назовите инструмент как
objdump
или жеnm
- Разбор объектных файлов самостоятельно (используя соответствующую библиотеку)
- Привлекайте анализатор и генерируйте необходимую информацию во время компиляции.
- "Использовать" компоновщик для создания массивов символов.
Я буду использовать немного базовых тестовых сред в качестве примеров ниже, потому что автоматическое обнаружение тестов для базовых тестовых сред является типичным примером, когда рефлексия очень удобна, и это то, чего не хватает большинству базовых тестовых сред для C.
С помощью libdl
(#include <dfcln.h>
) (POSIX)
Если вы находитесь в среде POSIX, немного размышлений можно сделать с помощью libdl
, Плагины разработаны таким образом.
использование
#include <dfcln.h>
в вашем исходном коде и ссылку с -ldl
,
Тогда у вас есть доступ к функциям dlopen()
, dlerror()
, dlsym()
а также dlclose()
с помощью которого вы можете загружать и получать доступ / запускать общие объекты во время выполнения. Тем не менее, это не дает вам легкий доступ к таблице символов.
Другим недостатком этого подхода является то, что вы в основном ограничивает отражение объектами, загруженными как динамическая библиотека (общий объект, загружаемый во время выполнения через dlopen()
).
Бег nm
или же objdump
Вы могли бы бежать nm
или же objdump
показать таблицу символов и проанализировать вывод. Для меня, nm -P --defined-only -g xyz.o
дает хорошие результаты, и анализ выходных данных тривиален. Вас заинтересует только первое слово в каждой строке, которое является именем символа, и, возможно, второе, которое является типом раздела.
Если вы не знаете имя объекта каким-либо статическим способом, то есть объект на самом деле является общим объектом, по крайней мере в Linux, вы можете пропустить имена символов, начинающиеся с '_'.
objdump
, nm
или подобные инструменты также часто доступны за пределами POSIX.
Разбор объектных файлов самостоятельно
Вы можете разобрать объектные файлы самостоятельно. Вы, вероятно, не хотите реализовать это с нуля, но используйте для этого существующую библиотеку. Вот как nm
, objdump
и даже libdl
реализованы. Вы можете посмотреть исходный код nm
, objdump
а также libdl
и библиотеки, которые они используют, чтобы узнать, как они делают то, что делают.
Вовлечение парсера
Вы можете написать анализатор и генератор кода, который генерирует необходимую отражающую информацию во время компиляции и сохраняет ее в объектном файле. Тогда у вас будет много свободы и вы сможете даже реализовать примитивные формы аннотаций. Это то, что делают некоторые фреймворки модульного тестирования, такие как AceUnit.
Я обнаружил, что написание синтаксического анализатора, который охватывает прямой синтаксис Си, довольно тривиально. Написание парсера, который действительно понимает C и может справиться со всеми случаями, НЕ тривиален. Таким образом, это имеет ограничения, которые зависят от того, насколько экзотичен синтаксис Си, о котором вы хотите подумать.
"Злоупотребление" компоновщиком для генерации символьных массивов
Вы можете поместить ссылки на символы, которые вы хотите отразить в специальном разделе, и использовать конфигурацию компоновщика, чтобы создать границы раздела, чтобы вы могли получить к ним доступ в C.
Я описал здесь внедрение N-зависимостей в C - лучший способ, чем массивы, определенные линкерами? как это работает
Но будьте осторожны, это зависит от многих вещей и не очень переносимо. Я только пробовал это с GCC
/ld
и я знаю, что это не работает со всеми компиляторами / компоновщиками. Кроме того, почти гарантировано, что удаление мертвого кода не будет определять, как вы называете этот материал, поэтому, если вы используете удаление мертвого кода, вам придется добавить все отраженные символы в качестве точек входа.
Ловушки
Для некоторых механизмов устранение мертвого кода может быть проблемой, в частности, когда вы "злоупотребляете" компоновщиком для создания массивов символов. Эту проблему можно обойти, указав отраженные символы как точки входа для компоновщика, и в зависимости от количества символов это может быть ни приятно, ни удобно.
Заключение
Объединяя nm
а также libdl
на самом деле может дать довольно хорошие результаты. Комбинация может быть почти такой же мощной, как уровень отражения, используемый JUnit 3.x в Java. Указанный уровень отражения достаточен для реализации инфраструктуры модульных тестов в стиле JUnit 3.x для C, включая обнаружение тестовых примеров в соответствии с соглашением об именах.
Привлечение синтаксического анализатора - это больше работы и ограничено объектами, которые вы сами компилируете, но дает вам больше силы и свободы. Указанный уровень отражения может быть достаточным для реализации среды модульного тестирования в стиле JUnit 4.x для C, включая обнаружение тестовых примеров по аннотациям. AceUnit - это модуль модульного тестирования для C, который делает именно это.
Объединение синтаксического анализа и компоновщика для генерации символьных массивов может дать очень хорошие результаты - если ваша среда настолько под вашим контролем, что вы можете гарантировать, что работа с компоновщиком таким образом работает для вас.
И, конечно, вы можете объединить все подходы, чтобы соединить кусочки и кусочки, пока они не будут соответствовать вашим потребностям.
Основываясь на ответах на Как я могу добавить отражение в приложение C++? (Переполнение стека) и тот факт, что C++ считается "надмножеством" C, я бы сказал, что вам не повезло.
Есть также хороший длинный ответ о том, почему C++ не имеет отражения (переполнение стека).
- Реализовать рефлексию для C было бы намного проще... потому что C - простой язык.
- Существует несколько основных опций для анализа программы, например, определение наличия функции с помощью вызова dlopen / dlsym - зависит от ваших потребностей.
- Существуют инструменты для создания кода, который может изменять / расширять себя, используя tcc.
- Вы можете использовать вышеуказанный инструмент для создания собственных анализаторов кода.
Вам нужно будет реализовать это с самого начала. В прямом C нет никакой информации о времени выполнения, которая бы хранилась о структуре и составных типах. Метаданные просто не существуют в стандарте.
По тем же причинам, что и автор вопроса, я работал над C-type-reflection-API вместе с форматом базы данных графа отражения C и подключаемым модулем clang, который записывает метаданные отражения.
Намерение состоит в том, чтобы использовать API отражения C для написания процедур сериализации и десериализации, таких как преобразователи для ASN.1, принтеры аргументов функций, прокси-серверы функций, фаззеры и т. Д. У Clang и GCC есть API-интерфейсы плагинов, которые разрешают доступ к AST, но есть в настоящее время нет стандартного графического формата для метаданных отражения C.
Предлагаемый API отражения C называется Crefl:
Crefl API обеспечивает доступ во время выполнения к метаданным отражения для объявлений структур C с поддержкой произвольно вложенных комбинаций: intrinsic, set, enum, struct, union, field (member), array, constant, variable.
- Формат базы данных графов отражений Crefl для переносимых метаданных отражений.
- Подключаемый модуль Crefl clang выводит метаданные отражения C, используемые библиотекой.
- Crefl API обеспечивает ориентированный на задачи доступ к метаданным отражения C по запросу.
API отражения AC обеспечивает доступ к метаданным отражения среды выполнения для объявлений структур C с поддержкой произвольно вложенных комбинаций: intrinsic, set, enum, struct, union, field, array, constant, variable. Модель данных отражения Crefl C по сути является транскрипцией типов данных C в ISO / IEC 9899:9999.
- Внутренние типы данных C.
- целые типы.
- типы с плавающей запятой.
- комплексные числовые типы.
- логический тип.
- вложенная структура, объединение, поле и битовое поле
- массивы и указатели
- псевдонимы типов typedef
- константы перечисления и перечисления
- функции и параметры функций
- квалификаторы const, volatile и restrict
- Атрибуты стиля GNU-C с использованием (
__attribute__
).
Библиотека все еще находится в стадии разработки. Надежда состоит в том, чтобы найти других, кто заинтересован в поддержке рефлексии в C.
Просто создайте таблицу с ключами, дерево, связанный список или любую коллекцию, с которой вам удобно работать или которая будет эффективной. Добавьте ключ, независимо от того, является ли он строкой, комбо типа / id или чем-то еще, и укажите адрес функции или структуры. Супер наивная версия отражения может быть коллекцией следующего:
struct reflectable{
size_t size,id,type; // describes payload
char* name;
void* payload;
}
С большим старым случаем переключения, в котором вы создаете обработчик для каждого типа, имени или макроса, чтобы присоединить одно и то же. В качестве альтернативы всегда присоединяйте функции, которые являются получателями того, что находится в вашей отражаемой структуре, которые совпадают с обработчиками, но с большей частью модели диспетчеризации, находящейся непосредственно в вашем контейнере.
Парсеры и символы отладки - отличные идеи. Тем не менее, проблема в том, что C на самом деле не имеет массивов. Просто указатели на вещи.
Например, невозможно прочитать исходный код, чтобы узнать, указывает ли символ * на символ, строку или фиксированный массив байтов на основе некоторого "близлежащего" поля длины. Это проблема для читателей, не говоря уже о любом автоматизированном инструменте.
Почему бы не использовать современный язык, такой как Java или.Net? Может быть быстрее, чем C.