I created some code in Rust that should allow me to set recursive Ports, Port having a source, that could have a source, etc. However, due to borrow issues the only way for me to implement this structure is through RefCell, and i actually do not want to use RefCell (it allows me to compile code that will potentially panic at runtime) or any other libraries from std.
The trouble is that it seems that i am unable to set a value to my Port due to borrow issues.
struct Port<'a> {
value: i32,
source: Option<&'a Port<'a>>,
}
impl<'a> Port<'a> {
fn new(value: i32) -> Port<'a> {
Port { value, source: None }
}
fn link(&mut self, source: &'a Port<'a>) {
self.source = Some(source);
}
fn get_source_value(&self) -> Option<i32> {
match self.source {
Some(port) => {
let inner_source_value = port.get_source_value();
match inner_source_value {
Some(value) => Some(value),
None => Some(port.value),
}
}
None => None,
}
}
}
fn main() {
let mut top_port = Port::new(10);
let mut sub_port = Port::new(20);
let mut sub_sub_port = Port::new(30);
sub_port.link(&top_port);
sub_sub_port.link(&sub_port);
top_port.value = 400; // can't do this because top_port is borrowed
println!("Value of sub_sub_port's source's source's source: {:?}", sub_sub_port.get_source_value());
}
The trouble occurs because top_port is borrowed when sub_port and sub_sub_port are linked to it. The fact that this happens is no surprise to me and i could use RefCell and RC to go around this issue.
use std::cell::RefCell;
struct Port<'a> {
value: i32,
source: Option<&'a RefCell<Port<'a>>>,
}
impl<'a> Port<'a> {
fn new(value: i32) -> RefCell<Port<'a>> {
RefCell::new(Port { value, source: None })
}
fn link(&mut self, source: &'a RefCell<Port<'a>>) {
self.source = Some(source);
}
fn get_source_value(&self) -> Option<i32> {
match self.source {
Some(port_ref) => {
let port = port_ref.borrow();
let inner_source_value = port.get_source_value();
match inner_source_value {
Some(value) => Some(value),
None => Some(port.value),
}
}
None => None,
}
}
}
fn main() {
let top_port = Port::new(10);
let sub_port = Port::new(20);
let sub_sub_port = Port::new(30);
sub_sub_port.borrow_mut().link(&sub_port);
sub_port.borrow_mut().link(&top_port);
top_port.borrow_mut().value = 400;
println!("Value of sub_sub_port's source's source's source: {:?}", sub_sub_port.borrow().get_source_value());
}
However, my goal is to not use any standard library functionality and to only use Rust in a safe way. In what way could i restructure my code to avoid having to use RefCell or any other std libraries? Or even better, could i use more lifetimes to fix my original code?
Thank you in advance!
As suggested in the comments, core::cell::Cell<i32>
will enable the replacement of the integer without requiring &mut
access.
Reading the integer with .get()
is possible because i32
is Copy
(the Cell
still has its content after reading it).
use core::cell::Cell;
struct Port<'a> {
value: Cell<i32>,
source: Option<&'a Port<'a>>,
}
impl<'a> Port<'a> {
fn new(value: i32) -> Port<'a> {
Port {
value: Cell::new(value),
source: None,
}
}
fn link(
&mut self,
source: &'a Port<'a>,
) {
self.source = Some(source);
}
fn get_source_value(&self) -> Option<i32> {
match self.source {
Some(port) => {
let inner_source_value = port.get_source_value();
match inner_source_value {
Some(value) => Some(value),
None => Some(port.value.get()),
}
}
None => None,
}
}
}
fn main() {
let top_port = Port::new(10);
let mut sub_port = Port::new(20);
let mut sub_sub_port = Port::new(30);
sub_port.link(&top_port);
sub_sub_port.link(&sub_port);
top_port.value.set(400);
println!(
"Value of sub_sub_port's source's source's source: {:?}",
sub_sub_port.get_source_value()
);
}
If the code has to be used in a parallel context, then we can replace the Cell<i32>
with an AtomicI32
.
Its .store()
function does not either need &mut
.
This is due to the fact that an atomic is fundamentally shared (otherwise the atomicity would not be required) while still allowing mutations (it's still interior mutability actually).
use core::sync::atomic::{AtomicI32, Ordering};
struct Port<'a> {
value: AtomicI32,
source: Option<&'a Port<'a>>,
}
impl<'a> Port<'a> {
fn new(value: i32) -> Port<'a> {
Port {
value: AtomicI32::new(value),
source: None,
}
}
fn link(
&mut self,
source: &'a Port<'a>,
) {
self.source = Some(source);
}
fn get_source_value(&self) -> Option<i32> {
match self.source {
Some(port) => {
let inner_source_value = port.get_source_value();
match inner_source_value {
Some(value) => Some(value),
None => Some(port.value.load(Ordering::Relaxed)),
}
}
None => None,
}
}
}
fn main() {
let top_port = Port::new(10);
let mut sub_port = Port::new(20);
let mut sub_sub_port = Port::new(30);
sub_port.link(&top_port);
sub_sub_port.link(&sub_port);
top_port.value.store(400, Ordering::Relaxed);
println!(
"Value of sub_sub_port's source's source's source: {:?}",
sub_sub_port.get_source_value()
);
}
In general, atomic operations are known to be less efficient than regular operations, because of the care taken by the hardware in order to always expose the data in a consistent state for all the cores/CPUs.
On x86(_64), there is no difference for load/store, but on other architectures this could be the case (however for add, sub... there always are penalties).
The relaxed memory-order used here should not introduce memory-barriers, but other ordering could also be detrimental to performances.