Как изменить код C++ из пользовательского ввода

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

Я хочу заменить что-либо в форме

A->Draw(B1, B2)

с

MyFunc(A, B1, B2).

Моей первой мыслью были регулярные выражения, но это было бы довольно подвержено ошибкам, поскольку любое из A, B1 или B2 могло быть произвольным выражением C++. Поскольку эти выражения сами могут содержать строки в кавычках или круглые скобки, было бы довольно сложно сопоставить все случаи с регулярным выражением. Кроме того, может быть несколько вложенных форм этого выражения.

Следующей моей мыслью было вызвать clang в качестве подпроцесса, использовать «-dump-ast», чтобы получить абстрактное синтаксическое дерево, изменить его, а затем перестроить в команду для передачи интерпретатору C++. Однако для этого потребуется отслеживать любые изменения среды, такие как включение файлов и предварительные объявления, чтобы предоставить clang достаточно информации для разбора выражения. Поскольку интерпретатор не раскрывает эту информацию, это также кажется неосуществимым.

Третья мысль заключалась в том, чтобы использовать собственный внутренний синтаксический анализ интерпретатора C++ для преобразования в абстрактное синтаксическое дерево, а затем строить оттуда. Однако этот интерпретатор никак не раскрывает ast, как мне удалось найти.

Есть ли предложения, как двигаться дальше, либо по одному из заявленных маршрутов, либо по другому маршруту совсем?


person Eldritch Cheese    schedule 04.08.2015    source источник
comment
Просто чтобы быть абсолютно уверенным, что вы не решаете здесь проблему x-y, можете ли вы рассказать нам, чего вы пытаетесь достичь, выполняя замену?   -  person Mark B    schedule 04.08.2015
comment
Интерпретатор C++ также имеет некоторые функции, доступные пользователю. Я хочу добавить в эти функции дополнительное поведение, но у них нет никакого механизма добавления в них хуков. Как только я передаю строку интерпретатору, я не могу восстановить управление напрямую. Поэтому я хочу изменить строку перед ее передачей, чтобы она передала мне управление в соответствующее время.   -  person Eldritch Cheese    schedule 04.08.2015
comment
Два вопроса: будут ли команды приходить каждой отдельной строкой? И можно ли увидеть повторные обращения к Draw в одной и той же строке? (A->Draw(...)->...->Draw(...))?   -  person Alexander Feterman    schedule 26.08.2015
comment
В одной строке может быть несколько команд, например, A->Draw(B); C->Draw(D) или func(A->Draw(B), C->Draw(D)). Повторных вызовов шаблона, который вы показали, не произойдет, так как Draw возвращает целочисленное значение.   -  person Eldritch Cheese    schedule 28.08.2015
comment
Может ли расширение макроса происходить при оценке A->Draw(B1, B2) ?   -  person serge-sans-paille    schedule 31.08.2015
comment
В принципе, макрорасширение могло произойти, и произошло бы после того, как управление покинет мою функцию. На практике я не видел здесь никаких макросов, поэтому я могу смело игнорировать раскрытие макросов.   -  person Eldritch Cheese    schedule 31.08.2015
comment
Вы хотите заменить одно произвольное выражение C++ (оператор/блок/...) другим? Если нет, то каковы ограничения на то, что можно заменить? Вы хотите заменить весь ввод (как с разделителями?) или вы хотите заменить подэлементы ввода? Если подэлементы, то как? Если подэлементы заменить только один, заменить все, что соответствует какой-то библиотеке? После замены может ли произойти еще одна замена? Как вы хотите выразить замены (поверхностный синтаксис? что-то еще)?   -  person Ira Baxter    schedule 19.12.2015


Ответы (6)


Вам нужна система преобразования программ. Это инструменты, которые обычно позволяют вам выражать изменения в исходном коде, написанные в шаблонах исходного кода, которые, по сути, говорят:

 if you see *this*, replace it by *that*

но работа с абстрактными синтаксическими деревьями, поэтому процесс сопоставления и замены гораздо более надежен, чем то, что вы получаете при взломе строк.

Такие инструменты должны иметь синтаксические анализаторы интересующего исходного языка. Исходный язык C++ делает это довольно сложным.

Clang подходит; в конце концов, он может анализировать C++. Объекты OP, он не может этого сделать без всего контекста среды. В той мере, в какой OP вводит (правильно сформированные) фрагменты программы (операторы и т. д.) в интерпретатор, Clang может [у меня нет большого опыта работы с этим] иметь проблемы с фокусировкой на том, что представляет собой фрагмент (оператор «выражение» объявление? ...). Наконец, Clang на самом деле не PTS; его процедуры модификации дерева не являются преобразованиями источника в источник. Это важно для удобства, но может не помешать OP использовать его; правило перезаписи синтаксиса поверхности удобно, но вы всегда можете заменить процедурный взлом дерева с большим усилием. Когда правил больше, чем несколько, это начинает иметь большое значение.

GCC с Melt квалифицируется так же, как и Clang. У меня сложилось впечатление, что Melt делает GCC в лучшем случае менее невыносимым для такой работы. YMMV.

Наш набор инструментов для реинжиниринга программного обеспечения DMS с его полный интерфейс C++14 [EDIT July 2018: C++17] абсолютно подходит. DMS использовался для выполнения массовых преобразований в крупномасштабных кодовых базах C++.

DMS может разбирать произвольные (правильно сформированные) фрагменты C++, не сообщая заранее, что такое синтаксическая категория, и возвращать AST надлежащего грамматического нетерминального типа, используя его механизм разбора шаблонов. [Вы можете получить несколько синтаксических анализов, например. двусмысленности, которые вы должны решить, как решить, см. 1004737">Почему C++ не может быть проанализирован с помощью синтаксического анализатора LR(1)? для более подробного обсуждения] Он может сделать это, не прибегая к «окружению», если вы готовы жить без раскрытия макросов во время синтаксического анализа, и настаивайте на том, чтобы директивы препроцессора (они тоже анализировались) были хорошо структурированы по отношению к фрагменту кода (#if foo{#endif не разрешено), но вряд ли это реальная проблема для фрагментов кода, введенных в интерактивном режиме.

Затем DMS предлагает полную процедурную библиотеку AST для манипулирования проанализированными деревьями (поиск, проверка, изменение, построение, замена), а затем может регенерировать поверхностный исходный код из модифицированного дерева, предоставляя текст OP для передачи интерпретатору.

В этом случае блестит то, что OP, вероятно, может написать большинство своих модификаций непосредственно в виде правил синтаксиса от источника к источнику. Для своего примера он может предоставить DMS правило перезаписи (непроверенное, но довольно близкое к правильному):

rule replace_Draw(A:primary,B1:expression,B2:expression):
        primary->primary
    "\A->Draw(\B1, \B2)"     -- pattern
rewrites to
    "MyFunc(\A, \B1, \B2)";  -- replacement

и DMS возьмет любой проанализированный AST, содержащий левосторонний шаблон "...Draw...", и заменит это поддерево правой частью после замены совпадений для A, B1 и B2. Кавычки являются метакавычками и используются, чтобы отличить текст C++ от текста правил синтаксиса; обратная косая черта — это метаэкранирование, используемое внутри метакавычек для обозначения метапеременных. Дополнительные сведения о том, что вы можете указать в синтаксисе правила, см. в правилах перезаписи DMS.

Если OP предоставляет набор таких правил, можно попросить DMS применить весь набор.

Так что я думаю, что это отлично сработает для OP. Это довольно тяжелый механизм для «дополнения» к пакету, который он хочет предоставить третьей стороне; DMS и его интерфейс C++ вряд ли можно назвать "маленькими" программами. Но тогда у современных машин много ресурсов, поэтому я думаю, что вопрос в том, насколько сильно OP нужно это делать.

person Ira Baxter    schedule 28.01.2016

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

Поскольку у вас есть интерпретатор C++ (как корень CERN), я думаю, вы должны использовать компилятор для перехвата всего Draw, простой и чистый способ сделать это — объявить в заголовках метод Draw как частный, используя некоторые определения.

 class ItemWithDrawMehtod
 {
 ....
 public:
 #ifdef CATCHTHEMETHOD
     private:
 #endif
 void Draw(A,B);
 #ifdef CATCHTHEMETHOD
     public:
 #endif
 ....
 };

Затем скомпилируйте как:

 gcc -DCATCHTHEMETHOD=1 yourfilein.cpp
person Kiko Albiol Colomer    schedule 06.09.2015
comment
Боюсь, я не уверен, какая от этого польза. Поскольку моя программа будет распространена среди других пользователей, которые будут компилировать немодифицированную версию библиотеки, изменение вызовов в самой библиотеке для вызова MyFunc вместо Draw не решит проблему. Кроме того, пользователи привыкли передавать интерпретатору Draw команд, и именно эти команды я хочу изменить. - person Eldritch Cheese; 16.09.2015
comment
Нет никакой реальной пользы, это решение предназначено не для производства, а для разработки. Суть в том, чтобы выдать принудительную ошибку и позволить компилятору сделать всю работу за вас. В исходном коде, за который вы отвечаете, вы можете использовать этот трюк, чтобы отловить все ошибки и предоставить свою функцию другим кодерам. Но это их ответственность за использование новой функции. - person Kiko Albiol Colomer; 18.09.2015

В случае, если пользователь хочет ввести в приложение сложные алгоритмы, я предлагаю интегрировать язык сценариев в приложение. Чтобы пользователь мог написать код [функцию/алгоритм определенным образом], чтобы приложение могло выполнить его в интерпретаторе и получить окончательные результаты. Например: Python, Perl, JS и т. д.

Поскольку вам нужен C++ в интерпретаторе, рекомендуется http://chaiscript.com/.

person Chand Priyankara    schedule 16.09.2015
comment
На данный момент уже существует язык сценариев C++. Хотя C++ обычно не используется в качестве языка сценариев, библиотека, которую я использую, включает интерпретатор C++. Я могу изменять команды до того, как они будут переданы в интерпретатор, но я не могу изменять сам код библиотеки, так как другие пользователи будут компилировать немодифицированную версию библиотеки. - person Eldritch Cheese; 16.09.2015

Что происходит, когда кто-то получает функцию-член Draw (auto draw = &A::Draw;), а затем начинает использовать draw? Вероятно, вы захотите, чтобы и в этом случае вызывалась та же улучшенная функциональность Draw. Таким образом, я думаю, мы можем сделать вывод, что вы действительно хотите заменить функцию-член Draw своей собственной функцией.

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

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

Другая возможность может состоять в том, чтобы использовать некоторые хакерские действия динамического компоновщика с использованием LD_PRELOAD для замены функции Draw, которая вызывается во время выполнения.

person hkBst    schedule 19.12.2015

В основном это можно сделать с помощью регулярных выражений.

Поскольку все, что появляется после Draw(, уже правильно отформатировано как параметры, вам не нужно полностью анализировать их для цели, которую вы изложили.

По сути, важная часть — это «SYMBOL->Draw(»

SYMBOL может быть любым выражением, которое разрешается в объект, который перегружает ->, или в указатель типа, который реализует Draw(...). Если вы уменьшите это до двух случаев, вы можете сократить синтаксический анализ.

В первом случае простое регулярное выражение, которое ищет любой допустимый символ C++, что-то вроде "[A-Za-z_][A-Za-z0-9_\.]", а также буквальное выражение "->Draw (". Это даст вам часть, которую необходимо переписать, поскольку код, следующий за этой частью, уже отформатирован как допустимые параметры C++.

Второй случай для сложных выражений, которые возвращают перегруженный объект или указатель. Это требует немного больше усилий, но короткая процедура синтаксического анализа для обхода сложного выражения в обратном направлении может быть написана на удивление легко, поскольку вам не нужно поддерживать блоки (блоки в C++ не могут возвращать объекты, поскольку определения лямбда-выражений не вызывают сами лямбды, а фактические вложенные блоки кода {...} не могут возвращать ничего непосредственно встроенного, что применимо здесь). Обратите внимание, что если выражение не заканчивается на ), то оно должно быть допустимым символом в этом контексте, поэтому, если вы найдете ) просто сопоставьте вложенный ) с ( и извлеките символ, предшествующий вложенному SYMBOL(...(.. .)...)->Draw() Это возможно с регулярными выражениями, но должно быть довольно просто и в обычном коде.

Как только у вас есть символ или выражение, замена тривиальна, начиная с

СИМВОЛ->Нарисовать(...

to

ВашаФункция(СИМВОЛ, ...

без необходимости иметь дело с дополнительными параметрами Draw().

Дополнительным преимуществом этой модели является то, что связанные вызовы функций анализируются бесплатно, поскольку вы можете рекурсивно перебирать код, например

A->Draw(B...)->Draw(C...)

Первая итерация идентифицирует первый A->Draw( и переписывает весь оператор как

YourFunction(A, B...)->Draw(C...)

который затем идентифицирует второй ->Draw с предшествующим ему выражением "YourFunction(A,...)->" и переписывает его как

YourFunction(YourFunction(A, B...), C...)

где B... и C... — правильно сформированные параметры C++, включая вложенные вызовы.

Не зная версии C++, которую поддерживает ваш интерпретатор, или типа кода, который вы будете переписывать, я действительно не могу предоставить какой-либо пример кода, который, вероятно, будет полезен.

person Matt Jordan    schedule 28.01.2016
comment
Мне кажется, вы утверждаете, что регулярные выражения могут анализировать произвольный C++. Неправда, что регулярные выражения могут анализировать любой контекстно-свободный язык, не говоря уже о C++. Возможно, вы сможете комбинировать пользовательское кодирование с регулярными выражениями, но, как правило, это безумие. Я думаю, что ваш подход был бы невероятно хрупким. Вы действительно где-то успешно реализовали эту идею? - person Ira Baxter; 28.01.2016
comment
@Ира Бакстер: Не обязательно. Я сказал, что это возможно с регулярными выражениями, но это должно быть довольно просто и в обычном коде. Существует разница между синтаксическим анализом кода с целью его компиляции и синтаксическим анализом кода с целью определить, где начинается/заканчивается выражение. Что касается того, реализовал ли я его, я написал компилятор C++ Server Page для собственного использования менее чем в двести строк C++ (преобразует в стандартный исходный код C++, который GCC или Visual Studio могут скомпилировать с предварительно созданным проектом), используя этот подход, но не с регулярными выражениями. Это может быть хрупким, это зависит. - person Matt Jordan; 28.01.2016
comment
Чтобы обрабатывать выражения C++, вы должны подобрать лексемы C++, согласны? Вы не можете разумно предположить, что вы можете написать лексер C++ в 200 строк, не говоря уже о том, чтобы выбирать подвыражения, поэтому я не понимаю, как вы это сделали точно. Во-вторых, некоторые вещи анализируются по-разному, когда интерпретируются как объявления и выражения; как ваша схема справляется с этим? Наконец, чтобы заменить выражения, вы должны найти их на всех уровнях оператора; это является синтаксическим анализом из-за вложенности. Мне трудно поверить в ваше подразумеваемое утверждение, что ваша серверная страница C++ обрабатывает произвольный C++. - person Ira Baxter; 29.01.2016
comment
Написание синтаксического анализатора C++, который генерирует деревья синтаксического анализа для полного компилятора, было бы очень трудным; однако написать синтаксический анализатор, который может различать и извлекать код C++ в блоках, на самом деле очень просто. Без необходимости построения полного дерева синтаксического анализа что-то вроде рекурсивного обхода кода довольно просто, поскольку C++ очень строг в отношении того, что может содержать код (например, может ли [ существовать в C++ без ]? Кроме escape-последовательности, может ли что-нибудь еще в строка влияет на ее пропуск?). OP не нужен код для перезаписи объявлений, только выражения, поэтому для этого контрпримера достаточно игнорирования объявлений. - person Matt Jordan; 02.02.2016
comment
У моей команды есть опыт написания полного клиентского интерфейса на C++; да, это сложно, тогда вы разберетесь с типами подвыражений. OP, похоже, вводит произвольные элементы кода C++ в свой интерпретатор. Выражения, которые он вводит, имеют типы. Даже если OP вводит тривиальные выражения, по-видимому, его замены должны быть совместимы по типу. Так что вам нужно не только парсить, но и разрешать имена в соответствии с правилами C++. Почему вы настаиваете на том, что это легко, я не понимаю. - person Ira Baxter; 02.02.2016

Один из способов - загрузить пользовательский код в виде DLL (что-то вроде плагинов), таким образом, вам не нужно компилировать ваше фактическое приложение, будет скомпилирован только пользовательский код, и ваше приложение будет загружать его динамически.

person alirakiyan    schedule 16.02.2016
comment
Хотя это это способ выполнения пользовательского кода в приложении, это не то, что нужно в данном случае (пользователь вводит активный код во время выполнения). - person crashmstr; 16.02.2016