Как стать автором
Обновить

Комментарии 25

На работе я переписываю запутанный C++ код на Rust

Зачем? )) Почему просто не переписать запутанный C++ код на распутанный?

Чтобы достичь похожего уровня безопасности (миллионы ассертов, концепты и тд) его придётся переписывать в другой запутанный код. И в итоге все равно надо будет отдельно ещё статический анализ делать.

похожего уровня безопасности

Какой безопасности? Тут как бы стиль с реинтерпрет кастами одной структуры в другую (aliasing rules? не, не слышал) уже о многом говорит. И никакие стандарт лэйауты тут не гарантия ничего. Да, в safe rust чувак не сможет так писать, но как только он начнёт писать unsafe код тут только держись, он и там будет рассуждать "ну байты и есть байты, ачотакова".

Откуда у вас появляются касты, когда вы переписываете с плюсов на плюсы? В случае с Rust там только FFI и остаётся с приколами, но это закономерно. В какой-то момент FFI исчезнет, либо превратится в небезопасные обёртки над безопасными функциями и количество проблем, которые решаются из коробки несопоставимы с тем что есть у C++ даже с каким-нибудь gsl.

Откуда у вас появляются касты, когда вы переписываете с плюсов на плюсы?

??? Вы о чем? При переписывании с плюсов на плюсы такие стремные касты ни к чему.

превратится в небезопасные обёртки над безопасными функциями

Которые в свою очередь являются "безопасными" обертками над небезопасными функциями, написанными неизвестно кем, и не исключено, что очередным чудаком, мыслящим в стиле "байты - они и в Африке байты".

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

Причина:

Это перевод. Какой-то чел топит за мега-глючный переход с C++ на Rust. Особенно доставило, что в оригинальной статье есть ссылка на донат.

В общем, всё как обычно :(.

класс C++ может содержать сотни методов

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

Понятно что в мире к сегодняшнему дню написаны тонны г@внокода. Его точно переносить куда-то надо?

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

Тем временем современный идиоматический ООП: существует ради полиморфизма подтипов, который реализуется виртуальными методами.

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

Я работаю с платформой IBM i где есть такая штука как ILE - интегрированная языковая среда. В двух словах - можно написать кусок кода на одном языке, кусок на другом, а потом все это собрать в один программный объект (бинарник). Равно как программа на том же С линкуется из нескольких .obj файлов, только каждый .obj (здесь это называется модуль - module) написан на разных языках.

Естественно, встает проблема стыковки интерфейсов - соответствие типов параметров, манглинг имен и т.п. Основные языки у нас - RPG, на котором пишется работа с БД и основная бизнес-логика и С/С++, на которых пишется всякое низкоуровневое ну и просто когда на С/С++ это написать проще и удобнее (RPG нельзя назвать универсальным языком).

Так вот, RPG не поддерживает ООП ни в каком виде. По уровню это что-то классического Паскаля. И проблема стыковки с тем, что написано на С++ решается через extern "C" врапперы для нужных вызовов.

В принципе, тут вполне реализуется даже работа с объектами - внутри С++ модуля делаем таблицу созданных объектов, враппер, создающий объект, вызывает конструктор, помещает указатель на созданный объект в таблицу и возвращает индекс в таблице (handle объекта). Ну а врапперы для методов вызываются с передачей этого самого handle и потом уже вызывают нужный метод для соответствующего объекта из таблицы.

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

И в реализации это проще чем описанное в статье шаманство (ну мне так кажется).

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

Тут дело в том, что не надо руками layout'ы совмещать. Все намного проще, враппер просто согласует интерфейсы.

Ну вот есть плюсовая функция

bool createQueObj(char* UsrQName, char* UsrQLib, eQueueType eSeq, 
                  char* UsrQDescr, int nMsgSize, int nKeyLen, 
                  int nInitMsgs, int nAddMsgs, int nMaxMsgs,
                  char* ErrorStr)

К ней есть враппер

extern "C" _RPG_ind USRQ_Create(char* __ptr128 Name, char* __ptr128 Lib, eQueueType eSeq, 
                                char* __ptr128 Desc, int nMsgSize, int nKeyLen, int nInitMsgs, 
                                int nExtMsgs, int nMaxMsgs, char* __ptr128 pError)
{
  _RPG_ind rslt = _ind_on_;
  char UsrQLib[11];
  char UsrQName[11];
  char UsrQDescr[51];
  char ErrorStr[37];

  if (pError) memset(pError, ' ', 37);

  if (Desc == NULL || strlen(Desc) == 0) strcpy(UsrQDescr, "*BLANKS");
  rpg2sz(UsrQName,  Name, 10);
  rpg2sz(UsrQLib,   Lib,  10);
  rpg2sz(UsrQDescr, Desc, 50);

  if (!createQueObj(UsrQName, UsrQLib, eSeq, UsrQDescr, nMsgSize, nKeyLen, 
                    nInitMsgs, nExtMsgs, nMaxMsgs, ErrorStr)) {
    if (pError) memcpy(pError, ErrorStr, 37);
    sendMessageToMSGQ((PStructError)ErrorStr, DEFMSGQUE);
    rslt = _ind_off_;
  }

  return rslt;
}

И вот враппер уже вызывается из RPG - там его прототип описан уже на RPG как некая "внешняя процедура"

dcl-pr USRQ_CreateQueue ind extproc(*CWIDEN : 'USRQ_Create') ;
  Name      char(10)                   const;                                  // Имя очереди
  Lib       char(10)                   const;                                  // Библиотека
  eSeq      int(10)                    value;                                  // Тип очереди queKeyd/queLIFO/queFIFO
  Desc      char(50)                   const;                                  // Описание
  nMsgSize  int(10)                    value;                                  // Макс. размер сообщения
                                                                               // Максимально допустимое значение - 64000 байт
  nKeyLen   int(10)                    value;                                  // Размер ключа (игнорируется для не queKeyd)
                                                                               // Максимально допустимое значение - 256 байт
  nInitMsgs int(10)                    value;                                  // Начальное количество сообщеий
  nExtMsgs  int(10)                    value;                                  // Колчество сообщений в приращении
  nMaxMsgs  int(10)                    value;                                  // Максимальное количество сообщений
  Error     char(37)                   options(*omit);                         // Ошибка
end-pr;

Аналогично и для врапперов, которые работают с объектами. Из RPG он будет вызываться с параметром hObj, а внутри вызовет нужный метод как objTbl[hObj]->func(...)

И никакой возни с layouts (если у нас что-то изменилось внутри С++ части, то пока это не влияет на контракты внешних интерфейсов, на RPG части это вообще никак не скажется).

Но тут еще вопрос изоляции (инкапсуляции) - все, что написано на С++ работает с теми объектами, которые характерны для С++. А врапперы вызываются из RPG и работают в терминах RPG. Таже std::string есть в С++, но ее нет в RPG. Там свои строки и они иначе релизованы. И со своими строками RPG работает весьма эффективно. А если мы попытаемся тащить туда std::string, то кроме кучи гимора не получим ничего. Поэтому из RPG во врапер приходит строка RPG, а С++ метод из врапера уже вызывается с std::string (условно говоря).

Иными словами, все эти layouts в RPG напрочь никому не нужны - что оно с ними будет делать. Нужны только конкретные интерфейсы для выполнения конкретных действий с теми объектами, с которыми работает RPG часть.

То есть оно до кучи ещё и виртуальные методы резолвит? Или только плоские типы?

А рядом с типом параметра это максимальный размер по указателю указан? или что значит char(10) в контексте декларации?

Оно резолвит все методы, для которых вы напишете врапперы. Но передавать туда, да только плоские типы с которым работает RPG. Но на самом деле этого вполне достаточно.

Тут суть в том, что все, что С++, оно внутри модуля на С++. Наружу выводятся только интерфейсы методов в виде врапперов. И вместо ссылки на объект класса выдается его хэндл в таблице который указывает на то, для какого именно объекта класса враппер вызовет данный метод.

char(10) - это в RPG строка 10 символов. Т.е.

char str[10];

в RPG будет

dcl-s str char(10);

С передачей строк из RPG в С/С++ есть несколько вариантов. Тот же

char* str

в параметре функции можно передать как

str char(10)

Если точно знаем что передается 10 символов. Или

str char(65536) options(*varsize)

если не знаем сколько передаем. Или

str pointer value options(*string: *trim)

но это будет

const char* str
dcl-s str char(10);

и присвоено

str = 'abc';

на самом деле будет

'abc       '

а при передаче с

pointer value otions(*string: *trim)

в С/С++ коде получим указатель на

'abc\0'

В общем, там есть определенные правила.

Поясню очень схематично.

Допустим, есть класс

class A {
  public:
    A(int a);
    ~A();
    int func(int b, int c);
  private:
    int aa;  
};

typdef A* PA;

И есть (где-то внутри С++ части) таблица существующих объектов этого класса + счетчик количества объектов в таблице

PA tblA[100];
int cntA = 0;

Пишем врапперы.

Конструктор создает новый объект класса, помещает его в свободное место в таблице и возвращает индекс этого объекта в таблице (его "handle")

extern "C" int crtA(int a)
{
  int h = cntA;
  
  tblA[h] = new A(a);
  cntA++;
  
  return h;
}

Деструктор - по хендлу объекта находит его в таблице и удаляет

extern "C" void delA(int h)
{
  if (tblA[h] != nullptr) {
    delete tblA[h];
    tblA[h] = nullptr;
  }
}

Метод. Опять по хендлу находит в таблице нужный объект и вызывает для него метод

extern "C" int funcA(int h, int b, int c)
{
  return (tblA[h] != nullptr) ? tblA[h]->func(b, c) : -1;
}

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

Аналогично - обработка исключений которые не прокинешь из С++ в другой язык. Тут уже надо думать как из враппера это возвращать наверх. Но тоже решаемо.

Как видите, все достаточно просто - все, что специфично для С++ содержится внутри С++ модуля. Наружу торчат только врапперы, которые адаптированы под тот язык, откуда они будут вызываться. Сами объекты классов маскируются хендлами объектов в таблице. Если у нас 10 объектов одного класса - будет 10 разных хендлов

На IBM i, где мы работаем есть еще такая штука как "группа активации" (activation group, AG), являющаяся подмножеством задания (job). И есть возможность повесить callback handler на закрытие ГА который будет автоматом вызываться систему перед завершением жизни группы активации. И там можно проверять - не осталось ли в таблице неудаленных объектов и корректно их удалять.

То есть из вариантов только хранить сами данные в контейнере, а в FFI отдавать только индексы в этом контейнере, а не указатели на сам объект?

Что здесь понимается под контейнером?

Это один программный объект ("объект" - это теминология IBM i - тут "все есть объект"), один бинарник. Просто часть его написана на С/С++, часть на RPG.

Проблема в том, что вы можете в RPG отдать указатель на объект С++ класса. Но для RPG это будет просто указатель. Он не знает про ООП и классы и вы чисто синтаксически не сможете описать прототип вызова метода класса на объект которого у вас есть указатель. Вы можете вызывать только функции.

Именно поэтому приходится рабоотать через врапперы для методов класса. А чтобы понимать для какого именно объекта вызывается этот метод использовать таблицу объектов и хендлы. Фактически это ручная реализация замены this.

Но это простой и понятный механизм. И универсальный - он будет работать для любого языка.

И да, врапперы могут использовать непосредственно указатель на объект класса (прямая эмуляция this) вместо хендла. Но это не кажется безопасным. В хендл можно кроме индекса в таблице еще и теги зашить и тогда есть передадите что-то совсем левое, оно просто выдаст ошибку при валидации.

И да, врапперы могут использовать непосредственно указатель на объект класса (прямая эмуляция this) вместо хендла. Но это не кажется безопасным.

Собственно вот это интересовало. Обычно за временем жизни следить морока.

Я в какой-то из ранних реализаций так делал - враппер конструктора возвращает сразу указатель на созданный объект класса.

Но потом это не понравилось - можно передать указатель на вообще что-то другое (во враппер для методов - он принимает не типизированный указатель - в RPG всего два типа - указатель на данные и указатель на процедуру) и тогда все валится. Или забыть удалить объект...

Поэтому перешел на описанный механизм с хендлами. Причем, старшее слово хендла (16бит) содержит некие теги, в т.ч. код типа объекта), а младшее слово (16 бит) - индекс в таблице объектов данного типа.

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

Но в целом эта схема работает для стыковки ООП языков с чисто процедурными. И не только в ситуации когда процедурная и ООП части сведены в одном бинарнике, но и в ситуации когда ООП часть вынесена в отдельную "сервисную программу" (аналог динамической библиотеки на IBM i) функции которой используются из процедурного языка.

Ну так это и есть стандартный способ биндинга. В свое время я кучу сишных и пару плюсовых библиотек к luajit через ffi прибиндил. Практически нигде не требовалось прокидывать лейаут структуры. Из исключений сходу могу только Windows API вспомнить, но там структуры изначально сишные и их код просто в неизменном виде копировался в текст биндинга.

Вот потому что не используете виртуальные функции, у вас и "сотни колбеков вздох". Такой подход я часто видел в C-программах, за неимением интерфейсов весь полиморфизм реализуется передачей колбеков. Часто даже делают аналог vtable, "закат солнца вручную".

Так вот что такое "без головной боли"... Не знал.

Ага, это даже без виртуальных функций(автору они не нужны) и тем более без множественного [и виртуального] на следования.

Ого, это что, адепты безопасного Rust агитируют небезопасно делать к области памяти небезопасного C++ в надежде что ничего не поломается?

Не обращайте внимания, странные люди везде есть.

В реальном коде все делают обертку на С (кодогенерацией с небольшими подпорками), над ней обертку на unsafe Rust (полностью автоматической кодогенерацией), и над ней обертку на safe Rust (вручную, но иногда часть удается автоматизировать). Вот хороший пример со всеми 3 уровнями оберток: https://github.com/twistedfall/opencv-rust.

А эти истории про переписывание из С++ на Rust скорее делают антирекламу, но по факту составляют примерно 0% кода.

Умиляют эти бесконечные статьи о переписывании с "небезопасных" плюсов на что-то очередное безопасное. Особенно прекрасно признание того, что все запутанно.

То есть говнокодеры, которые не сумели сделать нормальную архитектуру на плюсах, вдруг сделают её на другом языке? Смешно.

Следующий момент... Они никогда не трогают си, у них претензии именно к плюсам, особенно тем, в которых кто-то самовыражался в стиле си. Интересно, почему? Имея дело с кучей открытых проектов, я часто ловлю детские ошибки именно с сишным кодом.

В сумме видно, что тут простой попил бюджета с невероятным открытиями по поводу сишных интерфейсов. Переписывать с плюсов на очередной Х - это модно

Это не распил, всё проще. Выше написали:

Причина:

Это перевод. Какой-то чел топит за мега-глючный переход с C++ на Rust. Особенно доставило, что в оригинальной статье есть ссылка на донат.

В общем, всё как обычно :(.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий