Я перехожу на 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;
}
И еще: &mut &on_finish_launch
может быть просто &mut on_finish_launch
.
Вы не используете изменяемый доступ; тебе это действительно нужно?
Общая идея (в случае, если вам действительно нужен изменяемый доступ) состоит в том, чтобы передать &App
обратному вызову.
Использование Box<OsApp> на самом деле является пережитком предыдущего дизайна. Библиотека C включает указатель на App (как void *). который был передан через обратные вызовы
Хотя здесь он не используется, обратные вызовы будут изменять приложение, поэтому мне понадобится изменяемый доступ.
Я не думаю, что смогу передать &App в обратный вызов, потому что osapp и osapp_win находятся в отдельных библиотеках, где osapp зависит от osapp_win
Нужен ли on_finish_launch
доступ к osapp
?
В библиотеке C on_finish_launch вызывает osapp::cancel_user_attention(), который я здесь опустил.
Я имел в виду поле App::osapp
, а не ящик.
Извините, я не совсем ясно выразился. В оригинальной библиотеке C вызовом является osapp_cancel_user_attention(app->osapp);, который я определил как pub fn cancel_user_attention(_app: &OSApp).
Хорошо, тогда: нужен ли вам когда-нибудь изменяемый доступ к OSApp
, и если да, то нужен ли он on_finish_launch
или run()
?
@ChayimFriedman, да, обоим :-). Обновлен вопрос, чтобы указать, что это общий архитектурный шаблон исходной библиотеки.
Это действительно довольно сложно смоделировать в 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 нет замыканий :)
Отлично! Я знал, что мне придется подходить к вещам незнакомыми мне способами, и я определенно никогда не рассматривал этот дизайн. Спасибо.
Поскольку вы новичок, несколько замечаний: во-первых, все ваши
as_ref()
не нужны. При необходимости компилятор может выполнить автоматическое разыменование. Во-вторых, в этом кодеBox<OsApp>
может быть простоOsApp
(но, возможно,Box
необходим в вашем реальном коде). В-третьих, слушайте предупреждения компилятора. У вас есть три предупреждения: одно о том, что переменная не должна быть изменяемой, второе о неиспользуемом поле и третье о соглашениях об именах.