Skip to content

Instantly share code, notes, and snippets.

@Nekrolm
Last active March 2, 2024 18:54
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Nekrolm/de47ba3a2218c0ec865a91eb8f75cd91 to your computer and use it in GitHub Desktop.
Save Nekrolm/de47ba3a2218c0ec865a91eb8f75cd91 to your computer and use it in GitHub Desktop.
How to deal with ugly C callbacks

Мерзкий С-callback

Итак вы программист на высокоуровневых языках: Python, Java, C++ (прости господи)... И вот однажды вам, по долгу службы, выпадает необходимость взять и написать код на C/C++ с использованием прекрасной чистой C библиотеки... мы будем ее звать X.

В этой замечательной библиотеке есть функция

struct power_management_ctx_t; // opaque struct
typedef void(*event_callback)(int event_id);
void register_power_management_event_callback(
  power_management_ctx_t* ctx, 
  event_callback cbk
);

Вы уже давно травмированы объектно-ориентирванным программированием, поэтому для обработки эвентов у вас уже заготовлен класс:

struct EventHandler {
    ...
    void handle_event(int event_id);
};

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

Ведь вы же знаете, что у любого обычного метода класса есть неявный параметр -- ссылка на инстанс объекта, на котором вызывается метод. А тут что-то не так...

Нормальные C-style коллбеки имеют вид, например:

typedef ret_type(*normal_callback)(params..., void* user_data)

И нормальные функции регистрации таких коллбеков имеют вид, например:

void register_normal_callback(some_args..., normal_callback cbk, void* user_data)

И соответсвенно для регистрации вызова метода какого-то объекта в таком случае в C++ используют следующий паттерн

register_normal_callback(..., 
    +[](args..., void* user_data){ // плюс для явного приведения stateless лямбды к указателю на функцию
        static_cast<EventHandler*>(user_data)->handle_event(args...)
    },
    &handler_instance);

Но автор библиотели X был либо ленивым, либо человеконенавистником, либо и то и другое одновременно. Поэтому никакого параметра user_data он вам не предоставил. А делать что-то нужно...

Вариант 1. Глобальная переменная

Ну, самый "простой" и тупой вариант:

// заводим глобальную переменную
EventHandler* handler = nullptr;

void function_where_you_want_to_register_callback(...) {
   handler = &instance;
   register_power_management_event_callback(ctx, +[](int event_id)  {
      handler->handle_event(event_id);
   });
}

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

Вариант 2. Хвала дядюшке фон Нейману

Код это данные. Ну по крайней мере они лежат в одном и том же виртуальном адресном пространстве. Ну по крайней мере чаще всего это так.

Указатель на функцию -- это указатель на какие-то данные в памяти. Никто не запрещает нам самим в run-time сгенерировать код, который бы соответствовал дурной сигнатуре библиотечного коллбека, но при этом бы использовал указатель на наш объект EventHandler.

Итак у нас есть

typedef void(*ugly_c_callback_without_context)(int); // (1)
typedef void(*not_so_ugly_callback)(int, void* user_data); // (2)

И нам достаточно научиться превращать (2) в (1) в runtime. Для этого мы будем генерировать код для функции следующего вида:

// trampoline template -- used to get and check asm
extern "C" void ugly_callback_example(int x) {
    void* ptr = reinterpret_cast<void*>(0xAABBCCDDEEFFAABB); // заглушка для user_data
    not_so_ugly_callback cbc = reinterpret_cast<not_so_ugly_callback>(0xAAAAAAAAAAAAAAAA); // заглушка для указателя на трансформируемую функцию
    cbc(x, ptr);
}

Берем компилятор или идем на godbolt и генерируем оптимизированный asm для этой функции:

ugly_callback_example:
        movabs  rsi, -6144092013047338309
        movabs  rax, -6148914691236517206
        jmp     rax

Берем этот asm и генерируем коды инструкций

0:  48 be bb aa ff ee dd    movabs rsi,0xaabbccddeeffaabb
7:  cc bb aa
a:  48 b8 aa aa aa aa aa    movabs rax,0xaaaaaaaaaaaaaaaa
11: aa aa aa
14: ff e0                   jmp    rax

Копипастим инструкции и пишем наконец наш могучий конвертер:

ugly_c_callback_without_context make_code(void* user_data, not_so_ugly_callback cbk) {
    uint8_t ptr_bytes[sizeof(user_data)];
    uint8_t cbk_bytes[sizeof(cbk)];
    memcpy(ptr_bytes, &user_data, sizeof(user_data));
    memcpy(cbk_bytes, &cbk, sizeof(cbk));
    
    static_assert(sizeof(user_data) == 8);
    static_assert(sizeof(cbk) == 8);
    
    // заменяем заглушки на настоящие значения указателей
    uint8_t code[] = {
        0x48, 0xBE, ptr_bytes[0], ptr_bytes[1], ptr_bytes[2], ptr_bytes[3], 
                    ptr_bytes[4], ptr_bytes[5], ptr_bytes[6], ptr_bytes[7],
        
        0x48, 0xB8, cbk_bytes[0], cbk_bytes[1], cbk_bytes[2], cbk_bytes[3],
                    cbk_bytes[4], cbk_bytes[5], cbk_bytes[6], cbk_bytes[7],
        0xFF, 0xE0 
    };

    // аллоцируем исполнимую память!
    void* page = mmap(nullptr, sizeof(code), PROT_READ | PROT_WRITE | PROT_EXEC, 
    MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
    // здесь должна быть проверка что память выделена успешно, но мы в себе уверены
    memcpy(page, code, sizeof(code));
    // writable executable memory -- это потенциально ОГРОМНАЯ ДЫРЕНЬ, 
    // быстренько залатываем ее -- делаем readonly
    mprotect(page, sizeof(code), PROT_READ | PROT_EXEC);
    // мы восхитительны
    return reinterpret_cast<ugly_c_callback_without_context>(page);
}

Проверим, что все работает:

struct Foo {
    int data;
};

struct UglyRegister {
    ugly_c_callback_without_context cbk;

    void invoke(int val) {
        cbk(val);
    } 
};

int main() {

    Foo instance { 42 };

    UglyRegister reg {
        make_code(&instance, +[](int x, void* user_data){
            std::cout << "GOT IT: " <<static_cast<Foo*>(user_data)->data + x << std::endl;
        })
    };

    reg.invoke(55);
}

Отлично! Работает.

Кстати: https://github.com/libffi/libffi -- работает именно так.

Ну и самое главное: НЕ ДЕЛАЙТЕ ТАК.

Вариант 3. Невозможный

НАЙДИТЕ АВТОРА И ЗАСТАВЬТЕ ЕГО ИСПРАВИТЬ ЭТОТ КОШМАР.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment