Итак вы программист на высокоуровневых языках: 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
он вам не предоставил. А делать что-то нужно...
Ну, самый "простой" и тупой вариант:
// заводим глобальную переменную
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*
.
Код это данные. Ну по крайней мере они лежат в одном и том же виртуальном адресном пространстве. Ну по крайней мере чаще всего это так.
Указатель на функцию -- это указатель на какие-то данные в памяти. Никто не запрещает нам самим в 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 -- работает именно так.
Ну и самое главное: НЕ ДЕЛАЙТЕ ТАК.
НАЙДИТЕ АВТОРА И ЗАСТАВЬТЕ ЕГО ИСПРАВИТЬ ЭТОТ КОШМАР.