Как передать замыкания через необработанные указатели в качестве аргументов в функции C?
Я работаю с WinAPI в Rust и есть некоторые функции (например, EnumWindows()
) которые требуют обратного вызова. Обратный вызов обычно принимает дополнительный аргумент (типа LPARAM
который является псевдонимом для i64
), который можно использовать для передачи некоторых пользовательских данных в обратный вызов.
Пока я отправил Vec<T>
объекты как LPARAM к обратным вызовам WinAPI, и это работало нормально. Например "распаковка" lparam
значение для Vec<RECT>
выглядело так в моем случае:
unsafe extern "system" fn enumerate_callback(hwnd: HWND, lparam: LPARAM) -> BOOL {
let rects = lparam as *mut Vec<RECT>;
}
Вместо того, чтобы передавать вектор, теперь я должен передать функцию (или замыкание), поэтому в настоящее время мой код для распаковки выглядит следующим образом:
unsafe extern "system" fn enumerate_callback(hwnd: HWND, lparam: LPARAM) -> BOOL {
let cb: &mut FnMut(HWND) -> bool = &mut *(lparam as *mut c_void as *mut FnMut(HWND) -> bool);
...
}
Но это почему-то не работает, и я не могу понять, почему.
Я бы хотел знать:
- Есть ли способ передать функцию / замыкание в другую функцию и выполнить эти "C-подобные" приведения?
- Как правильно наложить закрытие на
i64
значение для передачи этого обратного вызова?
SSCCE:
use std::os::raw::c_void;
fn enum_wnd_proc(some_value: i32, lparam: i32) {
let closure: &mut FnMut(i32) -> bool = unsafe {
(&mut *(lparam as *mut c_void as *mut FnMut(i32) -> bool))
};
println!("predicate() executed and returned: {}", closure(some_value));
}
fn main() {
let sum = 0;
let mut closure = |some_value: i32| -> bool {
sum += some_value;
sum >= 100
};
let lparam = (&mut closure as *mut c_void as *mut FnMut(i32) -> bool) as i32;
enum_wnd_proc(20, lparam);
}
Я использую стабильную версию Rust.
1 ответ
Сначала несколько логических ошибок с кодом:
Неправильно ставить указатели на
i32
на многих платформах (например, 64-битных). Указатели могут использовать все эти биты. Усечение указателя и последующий вызов функции по усеченному адресу приведет к действительно плохим вещам. Как правило, вы хотите использовать целое число размером с машину (usize
или жеisize
).sum
значение должно быть изменчивым.
Суть проблемы в том, что замыкания являются конкретными типами, которые занимают размер, неизвестный программисту, но известный компилятору. Функция C ограничена получением целого числа машинного размера.
Потому что замыкания реализуют один из Fn*
traits, мы можем ссылаться на реализацию этой черты замыканием для генерации объекта trait. Взятие ссылки на черту приводит к толстому указателю, который содержит два значения размера указателя. В этом случае он содержит указатель на закрытые данные и указатель на виртуальную таблицу, конкретные методы, которые реализуют эту черту.
В общем, любая ссылка или Box
нестандартного типа будет генерировать толстый указатель.
На 64-битной машине полный указатель будет иметь в общей сложности 128 битов, и приведение его к указателю размера машины снова урежет данные, что приведет к возникновению действительно плохих вещей.
Решение, как и все остальное в информатике, состоит в том, чтобы добавить больше уровней абстракции:
use std::mem;
use std::os::raw::c_void;
fn enum_wnd_proc(some_value: i32, lparam: usize) {
let closure: &mut &mut FnMut(i32) -> bool = unsafe { mem::transmute(lparam as *mut c_void) };
println!("predicate() executed and returned: {}", closure(some_value));
}
fn main() {
let mut sum = 0;
let mut closure = |some_value: i32| -> bool {
println!("I'm summing {} + {}", sum, some_value);
sum += some_value;
sum >= 100
};
let mut trait_obj: &mut FnMut(i32) -> bool = &mut closure;
let closure_pointer_pointer: *mut c_void = unsafe { mem::transmute(&mut trait_obj) };
let lparam = closure_pointer_pointer as usize;
enum_wnd_proc(20, lparam);
}
Мы берем вторую ссылку на толстый указатель, который создает тонкий указатель. Этот указатель имеет только одно целочисленное значение.
Может, диаграмма поможет (или навредит)?
reference -> Trait object -> Concrete closure
8 bytes 16 bytes ?? bytes
Поскольку мы используем необработанные указатели, теперь программисты обязаны убедиться, что замыкание переживает то место, где оно используется! Если enum_wnd_proc
где-то хранит указатель, вы должны быть очень осторожны, чтобы не использовать его после снятия замыкания.
Как примечание стороны, используя mem::transmute
при наложении черты объекта:
let closure_pointer_pointer: *mut c_void = unsafe { mem::transmute(trait_obj) };
Создает лучшее сообщение об ошибке:
error: transmute called with differently sized types:
&mut std::ops::FnMut(i32) -> bool (128 bits) to
*mut std::os::raw::c_void (64 bits)
Смотрите также