Работа с проблемными родительско-дочерними отношениями, навязанными C FFI - PullRequest
0 голосов
/ 05 июля 2018

У меня есть библиотека C с интерфейсом, похожим на этот: (я представлял C API в Rust, так что весь код в этом вопросе можно объединить в один файл .rs и легко протестировать)

// Opaque handles to C structs
struct c_A {}
struct c_B {}

// These 2 `create` functions allocate some heap memory and other 
// resources, so I have represented this using Boxes.
extern "C" fn create_a() -> *mut c_A {
    let a = Box::new(c_A {});
    Box::into_raw(a)
}

// This C FFI function frees some memory and other resources, 
// so I have emulated that here.
extern "C" fn destroy_a(a: *mut c_A) {
    let _a: Box<c_A> = unsafe { Box::from_raw(a) };
}

extern "C" fn create_b(_a: *mut c_A) -> *mut c_B {
    let b = Box::new(c_B {});
    Box::into_raw(b)
}

// Note: While unused here, the argument `_a` is actually used in 
// the C library, so I cannot remove it. (Also, I don't control 
// the C interface)
extern "C" fn destroy_b(_a: *mut c_A, b: *mut c_B) {
    let _b = unsafe { Box::from_raw(b) };
}

Я создал следующую Расти-абстракцию над функциями C:

struct A {
    a_ptr: *mut c_A,
}

impl A {
    fn new() -> A {
        A { a_ptr: create_a() }
    }
}

impl Drop for A {
    fn drop(&mut self) {
        destroy_a(self.a_ptr);
    }
}

struct B<'a> {
    b_ptr: *mut c_B,
    a: &'a A,
}

impl<'a> B<'a> {
    fn new(a: &'a A) -> B {
        B {
            b_ptr: create_b(a.a_ptr),
            a,
        }
    }
}

impl<'a> Drop for B<'a> {
    fn drop(&mut self) {
        destroy_b(self.a.a_ptr, self.b_ptr);
    }
}

Структура B содержит ссылку на A по единственной причине, что a_ptr необходим при вызове функции destroy_b для очистки памяти. Эта ссылка мне не нужна ни для одного из моих кодов Rust.

Теперь я хотел бы создать следующую структуру, которая ссылается как на A, так и на B:

struct C<'b> {
    a: A,
    b: B<'b>,
}

impl<'b> C<'b> {
    fn new() -> C<'b> {
        let a = A::new();
        let b = B::new(&a);
        C { a, b }
    }
}

// Main function just so it compiles
fn main() {
    let c = C::new();
}

Однако, это не скомпилируется:

error[E0597]: `a` does not live long enough
  --> src/main.rs:76:25
   |
76 |         let b = B::new(&a);
   |                         ^ borrowed value does not live long enough
77 |         C { a, b }
78 |     }
   |     - borrowed value only lives until here
   |
note: borrowed value must be valid for the lifetime 'b as defined on the impl at 73:1...
  --> src/main.rs:73:1
   |
73 | impl<'b> C<'b> {
   | ^^^^^^^^^^^^^^

Я понимаю, почему это не получается: при возврате структуры C из C::new() она перемещает C. Это означает, что A, содержащийся внутри, перемещен, что делает все ссылки на него недействительными. Поэтому я не могу создать эту C структуру. ( Более подробно объяснено здесь )

Как я могу реорганизовать этот код таким образом, чтобы я мог хранить B в структуре вместе с ее "родительским" A? У меня есть несколько вариантов, которые не будут работать:

  • Изменение интерфейса C: я не контролирую интерфейс C, поэтому не могу его изменить.
  • Пусть B хранит *mut c_A вместо &A: если A отброшен, то этот необработанный указатель становится недействительным и приведет к неопределенному поведению при отбрасывании B.
  • Пусть B хранит принадлежащий A, а не ссылку &A: для моего случая использования я должен иметь возможность создавать несколько B с для каждого A. Если B принадлежит A, то каждый A может использоваться только для создания одного B.
  • Имеет A все экземпляры B и возвращает только ссылки на B при создании нового B: проблема в том, что B s будет накапливаться со временем до тех пор, пока A не станет упал, занимая больше памяти, чем необходимо. Однако, если это действительно лучший способ, я могу справиться с небольшим неудобством.
  • Используйте ящик rental: я предпочел бы получить небольшое использование памяти, чем добавить сложность нового макроса в мой код. (То есть сложность любого, кто читает мой код, должен узнать, как работает этот макрос)

Я подозреваю, что лучшее решение каким-то образом предполагает хранение по крайней мере A в куче, так что ему не нужно перемещаться, но я не могу понять, как заставить это работать. Кроме того, мне интересно, есть ли что-нибудь умное, что я могу сделать, используя необработанные указатели.

1 Ответ

0 голосов
/ 06 июля 2018

Это звучит как идеальный случай для подсчета ссылок. Используйте Rc или Arc, в зависимости от ваших потребностей многопоточности:

use std::rc::Rc;

struct B {
    b_ptr: *mut c_B,
    a: Rc<A>,
}

impl B {
    fn new(a: Rc<A>) -> B {
        B {
            b_ptr: create_b(a.a_ptr),
            a,
        }
    }
}

impl Drop for B {
    fn drop(&mut self) {
        destroy_b(self.a.a_ptr, self.b_ptr);
    }
}

fn main() {
    let a = Rc::new(A::new());
    let x = B::new(a.clone());
    let y = B::new(a);
}
  • Не меняет интерфейс C.
  • A не может быть отброшен, пока есть еще B s, ссылающиеся на него.
  • Может создавать несколько B с для каждого A.
  • A не будет увеличиваться вечно.
  • Создает одно выделение кучи для хранения A и его счетчика ссылок.
  • Rc находится в стандартной библиотеке, нет нового ящика для изучения.

В будущем вы сможете использовать произвольные типы себя , чтобы написать это лучше:

#![feature(arbitrary_self_types)]

use std::rc::Rc;

struct A {
    a_ptr: *mut c_A,
}

impl A {
    fn new() -> A {
        A { a_ptr: create_a() }
    }

    fn make_b(self: &Rc<Self>) -> B {
        B {
            b_ptr: create_b(self.a_ptr),
            a: self.clone(),
        }
    }
}

impl Drop for A {
    fn drop(&mut self) {
        destroy_a(self.a_ptr);
    }
}

struct B {
    b_ptr: *mut c_B,
    a: Rc<A>,
}

impl Drop for B {
    fn drop(&mut self) {
        destroy_b(self.a.a_ptr, self.b_ptr);
    }
}

fn main() {
    let a = Rc::new(A::new());
    let x = a.make_b();
    let y = a.make_b();
}
...