Как передать замыкания через необработанные указатели в качестве аргументов в функции 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);
    ...
}

Но это почему-то не работает, и я не могу понять, почему.

Я бы хотел знать:

  1. Есть ли способ передать функцию / замыкание в другую функцию и выполнить эти "C-подобные" приведения?
  2. Как правильно наложить закрытие на 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 ответ

Решение

Сначала несколько логических ошибок с кодом:

  1. Неправильно ставить указатели на i32 на многих платформах (например, 64-битных). Указатели могут использовать все эти биты. Усечение указателя и последующий вызов функции по усеченному адресу приведет к действительно плохим вещам. Как правило, вы хотите использовать целое число размером с машину (usize или же isize).

  2. 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)

Ошибка E0512.


Смотрите также

Другие вопросы по тегам