Перенос кода C с изменяемыми заимствованиями — ошибка во время выполнения (пример nappgui)

Я перехожу на Rust, имея опыт системного программирования на C/C++. Закончив официальную книгу и курс Rustlings, я решил портировать библиотеку nappgui (https://github.com/frang75/nappgui), чтобы укрепить свое понимание Rust.

В nappgui часто используется определенный шаблон, который мне сложно эффективно перевести на Rust. Шаблон включает в себя изменяемые заимствования, и пока моя текущая реализация компилируется, выдает ошибки времени выполнения, связанные с этими заимствованиями.

Вот минимальный пример моей последней попытки (хотя он может не совсем отражать исходный код nappgui):

//////////////////////////////////////////////////////////
// Platform specific OSAPP library crate
// e.g. osapp_win.rs

pub struct OSApp {
    abnormal_termination: bool,
    with_run_loop: bool,
}

pub fn init_imp(with_run_loop: bool) -> Box<OSApp> {
    Box::new(OSApp {
        abnormal_termination: false,
        with_run_loop: with_run_loop,
    })
}

pub fn run(app: &OSApp, on_finish_launching: &mut dyn FnMut()) {
    on_finish_launching();

    if app.with_run_loop {
        // Following line commented out to simplify
        // osgui::message_loop();
        i_terminate(app);
    }
}

fn i_terminate(_app: &OSApp) {
    // Calls more client callbacks
}


//////////////////////////////////////////////////////////
// OSAPP crate

use core::f64;
use std::{cell::RefCell, rc::Rc};

struct App {
    osapp: Option<Box<OSApp>>,
    _lframe: f64,
    func_create: FnAppCreate,
}

pub trait ClientObject {}
type FnAppCreate = fn() -> Box<dyn ClientObject>;

pub fn osmain(lframe: f64, func_create: FnAppCreate) {
    let app = Rc::new(RefCell::new(App {
        osapp: None,
        _lframe: lframe,
        func_create: func_create,
    }));

    let osapp: Box<OSApp> = osapp_init(true);

    let tmp_a = app.clone();
    tmp_a.as_ref().borrow_mut().osapp = Some(osapp);

    let tmp_b = app.clone();
    let on_finish_launch = || {
        // I understand why I get the already borrowed mutable error here
        i_OnFinishLaunching(&tmp_b.as_ref().borrow());
        // ^^^^^^^^^^^^^^^^^^^^^^^^^
    };

    let tmp_c = &app.as_ref().borrow_mut().osapp;
    if let Some(osapp) = tmp_c {
        /*osapp::*/
        run(&osapp, &mut &on_finish_launch);
    }
}

fn osapp_init(with_run_loop: bool) -> Box<OSApp> {
    /*osapp::*/
    init_imp(with_run_loop)
}

fn i_OnFinishLaunching(app: &App) {
    (app.func_create)();
}

//////////////////////////////////////////////////////////
// main.rs

struct Application {
    // widgets go here
}

impl ClientObject for Application {}

impl Application {
    fn create() -> Box<dyn ClientObject> {
        let mut app = Box::new(Application {
            // Create all the widgets here
        });

        app
    }
}

fn main() {
    /*osapp::*/
    osmain(0.0, Application::create);
}

Выход:

thread 'main' panicked at src/main.rs:55:45:
already mutably borrowed: BorrowError
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Я был бы признателен за некоторые рекомендации о том, как перепроектировать или реализовать код, чтобы избежать этих изменяемых ошибок заимствования. Любая информация, касающаяся переноса шаблонов C++, включающих изменяемые заимствования, в Rust, будет особенно полезна.

Обновлять

Вышеупомянутое является примером архитектурного шаблона, используемого на протяжении всего проекта. Общая схема в C выглядит следующим образом:

#include <stdio.h>
#include <stdlib.h>

typedef void(*CB1_T)(void*) ;
typedef void(*CB2_T)(void*) ;

struct LowLevelObject {
    void *listener;
    // state data
    CB1_T callback1;
    CB2_T callback2;
};

void low_api1(struct LowLevelObject *lo)
{
    // some functionality
    lo->callback1(lo->listener);
    // more functionality
}

void low_api2(struct LowLevelObject *lo)
{
    // some functionality
    lo->callback2(lo->listener);
    // more functionality
}

void low_api3(struct LowLevelObject *lo)
{
    // some functionality
    printf("%s\n", __func__);
}


struct LowLevelObject *low_create(
    void *listener,
    CB1_T callback1,
    CB2_T callback2)
{
    struct LowLevelObject *lo = calloc(1, sizeof(struct LowLevelObject));
    lo->listener = listener;
    lo->callback1 = callback1;
    lo->callback2 = callback2;
    
    return lo;
}

void low_destroy(struct LowLevelObject *lo)
{
    free(lo);
}

/////////////////////////////////////////

struct HighLevelObject {
    struct LowLevelObject *low;
    // State data
};

static void on_callback1(struct HighLevelObject *hi)
{
    printf("%s\n", __func__);
    low_api3(hi->low);
}

static void on_callback2(struct HighLevelObject *hi)
{
    printf("%s\n", __func__);
    low_api3(hi->low);
}

struct HighLevelObject *high_create()
{
    struct HighLevelObject *hi = calloc(1, sizeof(struct HighLevelObject));
    
    hi->low = low_create(hi, (CB1_T)on_callback1, (CB2_T)on_callback2);
    // NULL checks ignored for simplicity
    return hi;
}

void high_destroy(struct HighLevelObject *hi)
{
    low_destroy(hi->low);
    free(hi);
}

void hi_start()
{
    struct HighLevelObject *hi = high_create();
    low_api1(hi->low);
    low_api2(hi->low);
    high_destroy(hi);
}

////////////////////////////////

int main() {
    hi_start();
    
    return 0;
}

Поскольку вы новичок, несколько замечаний: во-первых, все ваши as_ref() не нужны. При необходимости компилятор может выполнить автоматическое разыменование. Во-вторых, в этом коде Box<OsApp> может быть просто OsApp (но, возможно, Box необходим в вашем реальном коде). В-третьих, слушайте предупреждения компилятора. У вас есть три предупреждения: одно о том, что переменная не должна быть изменяемой, второе о неиспользуемом поле и третье о соглашениях об именах.

Chayim Friedman 25.04.2024 13:57

И еще: &mut &on_finish_launch может быть просто &mut on_finish_launch.

Chayim Friedman 25.04.2024 13:59

Вы не используете изменяемый доступ; тебе это действительно нужно?

Chayim Friedman 25.04.2024 14:00

Общая идея (в случае, если вам действительно нужен изменяемый доступ) состоит в том, чтобы передать &App обратному вызову.

Chayim Friedman 25.04.2024 14:05

Использование Box<OsApp> на самом деле является пережитком предыдущего дизайна. Библиотека C включает указатель на App (как void *). который был передан через обратные вызовы

Dushara 25.04.2024 14:08

Хотя здесь он не используется, обратные вызовы будут изменять приложение, поэтому мне понадобится изменяемый доступ.

Dushara 25.04.2024 14:09

Я не думаю, что смогу передать &App в обратный вызов, потому что osapp и osapp_win находятся в отдельных библиотеках, где osapp зависит от osapp_win

Dushara 25.04.2024 14:14

Нужен ли on_finish_launch доступ к osapp?

Chayim Friedman 25.04.2024 14:18

В библиотеке C on_finish_launch вызывает osapp::cancel_user_attention(), который я здесь опустил.

Dushara 25.04.2024 22:31

Я имел в виду поле App::osapp, а не ящик.

Chayim Friedman 25.04.2024 22:32

Извините, я не совсем ясно выразился. В оригинальной библиотеке C вызовом является osapp_cancel_user_attention(app->osapp);, который я определил как pub fn cancel_user_attention(_app: &OSApp).

Dushara 25.04.2024 22:38

Хорошо, тогда: нужен ли вам когда-нибудь изменяемый доступ к OSApp, и если да, то нужен ли он on_finish_launch или run()?

Chayim Friedman 25.04.2024 22:45

@ChayimFriedman, да, обоим :-). Обновлен вопрос, чтобы указать, что это общий архитектурный шаблон исходной библиотеки.

Dushara 26.04.2024 12:23
Почему Python в конце концов умрет
Почему Python в конце концов умрет
Последние 20 лет были действительно хорошими для Python. Он прошел путь от "просто языка сценариев" до основного языка, используемого для написания...
2
13
70
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Это действительно довольно сложно смоделировать в Rust. Давайте проанализируем, что мы здесь имеем:

У нас есть два пакета, low и high, которые зависят друг от друга, создавая цикл. Поскольку пакет low не знает о high, они создают этот цикл со стиранием типов и циклами в структуре данных.

Rust не любит циклы. Действительно, нет. Циклы в структурах данных практически невозможно создавать и управлять ими, и этот тип циклов не является исключением. Вы пытались проявить смекалку и избежать цикла, используя дерево композиции вместо графика, но цикл неизбежен. Вы избежали цикла в данных, но создали цикл в потоке кода.

Итак, вся надежда потеряна?

Нисколько. Нам просто нужно быть немного умнее.

Решение — инвертировать композицию.

Вы пытались сдержать LowLevelObject в HighLevelObject. С концептуальной точки зрения это имеет смысл: в конце концов, композиция (как и наследование) берет существующие типы и добавляет к ним функциональность, поэтому имеет смысл включать «меньшие» типы в «большие» типы, не так ли?

Да. Но иногда мир заставляет нас идти против логики.

Когда мы сохраняем LowLevelObject в HighLevelObject, мы должны хранить обратные вызовы в LowLevelObject. Мы не можем хранить их в HighLevelObject, так как тогда LowLevelObject не сможет их найти. Но обратным вызовам нужна ссылка на HighLevelObject, а LowLevelObject не может дать им этого сам по себе, а HighLevelObject не может помочь, потому что это нарушит правила псевдонимов.

Другими словами, обратные вызовы должны храниться в структуре верхнего уровня. Это потому, что им нужен доступ как к HighLevelObject (напрямую), так и к LowLevelObject (косвенно, через его методы). Единственный тип, который может предоставить им это, — это тип, который содержит и то, и другое. Но с другой стороны, обратные вызовы не могут храниться в HighLevelObject, поскольку LowLevelObject необходимо их вызывать. Очевидным решением этих требований является наличие LowLevelObject внутри HighLevelObject.

Вы спросите, не нарушит ли это инкапсуляцию? Этого не произойдет, потому что мы удалим тип HighLevelObject - через дженерики или динамическую отправку.

Вот как это будет выглядеть:

// crate `low`

pub struct LowLevelObject<T> {
    pub listener: T,
    callback1: fn(&mut Self),
    callback2: fn(&mut Self),

    example_low_level_data: String,
}

impl<T> LowLevelObject<T> {
    pub fn low_api1(&mut self) {
        // some functionality
        (self.callback1)(self);
        // more functionality
    }

    pub fn low_api2(&mut self) {
        // some functionality
        (self.callback2)(self);
        // more functionality
    }

    pub fn low_api3(&mut self) {
        // some functionality
        println!("`low_api3()` - {}", self.example_low_level_data);
    }

    pub fn create(listener: T, callback1: fn(&mut Self), callback2: fn(&mut Self)) -> Self {
        Self {
            listener,
            callback1,
            callback2,
            example_low_level_data: "this is low-level data".to_owned(),
        }
    }
}

// crate `high`

pub struct HighLevelState {
    example_high_level_data: String,
}

pub type HighLevelObject = LowLevelObject<HighLevelState>;

// Define them either as free functions or in extension traits, whatever makes you feel better.

fn on_callback1(hi: &mut HighLevelObject) {
    println!("`on_callback1()` - {}", hi.listener.example_high_level_data);
    hi.low_api3();
}

fn on_callback2(hi: &mut HighLevelObject) {
    println!("`on_callback2()` - {}", hi.listener.example_high_level_data);
    hi.low_api3();
}

pub fn high_create() -> HighLevelObject {
    LowLevelObject::create(
        HighLevelState {
            example_high_level_data: "this is high-level data".to_owned(),
        },
        on_callback1,
        on_callback2,
    )
}

И вот ваш исходный код:

//////////////////////////////////////////////////////////
// Platform specific OSAPP library crate
// e.g. osapp_win.rs

pub struct OSApp<App> {
    pub app: App,
    abnormal_termination: bool,
    with_run_loop: bool,
}

pub fn init_imp<App>(app: App, with_run_loop: bool) -> OSApp<App> {
    OSApp {
        app,
        abnormal_termination: false,
        with_run_loop,
    }
}

pub fn run<App>(app: &mut OSApp<App>, on_finish_launching: &mut dyn FnMut(&mut OSApp<App>)) {
    on_finish_launching(app);

    if app.with_run_loop {
        // Following line commented out to simplify
        // osgui::message_loop();
        i_terminate(app);
    }
}

fn i_terminate<App>(_app: &OSApp<App>) {
    // Calls more client callbacks
}

//////////////////////////////////////////////////////////
// OSAPP crate

use core::f64;
use std::{cell::RefCell, rc::Rc};

struct AppState {
    _lframe: f64,
    func_create: FnAppCreate,
}

type App = OSApp<AppState>;

pub trait ClientObject {}
type FnAppCreate = fn() -> Box<dyn ClientObject>;

pub fn osmain(lframe: f64, func_create: FnAppCreate) {
    let mut app = init_imp(
        AppState {
            _lframe: lframe,
            func_create: func_create,
        },
        true,
    );

    let mut on_finish_launch = |app: &mut App| i_OnFinishLaunching(app);

    /*osapp::*/
    run(&mut app, &mut on_finish_launch);
}

fn i_OnFinishLaunching(app: &App) {
    (app.app.func_create)();
}

//////////////////////////////////////////////////////////
// main.rs

struct Application {
    // widgets go here
}

impl ClientObject for Application {}

impl Application {
    fn create() -> Box<dyn ClientObject> {
        let mut app = Box::new(Application {
            // Create all the widgets here
        });

        app
    }
}

fn main() {
    /*osapp::*/
    osmain(0.0, Application::create);
}

Примечание: если вы хотите сохранить замыкания захвата в обратных вызовах, вы столкнетесь с некоторыми проблемами. Их можно исправить с помощью некоторых хитростей, но я не думаю, что они вам понадобятся, поскольку вы портируете библиотеку C, а в C нет замыканий :)

Отлично! Я знал, что мне придется подходить к вещам незнакомыми мне способами, и я определенно никогда не рассматривал этот дизайн. Спасибо.

Dushara 28.04.2024 02:32

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

Не могу заимствовать *self как неизменяемый, но не могу найти обходной путь
Возвращает принадлежащее значение и ссылку на значение
Заимствовать некоторое изменяемое значение дважды, если известно, что изменяемое значение является неизменяемым
Почему мой параметр типа в этом блоке impl не ограничен?
Какую структуру можно создать, чтобы избежать использования RefCell?
Почему средство проверки заимствований в Rust жалуется при использовании итератора, возвращаемого из метода, но не при непосредственном использовании итератора Vec?
Как лучше всего распараллелить код, изменяя несколько фрагментов одного и того же вектора Rust?
Rust: вернуть неизменяемый заем после изменения
Каким был бы идиоматический способ Rust иметь вектор признаков с псевдонимами для отдельных элементов вектора?
Как вернуть ссылку на значение внутри Rc<RefCell<Node>>