forked from ROMEO/obsw
Merge pull request 'mohr/rust' (#2) from mohr/rust into nehlich/rust-readme
Reviewed-on: romeo/obsw#2
This commit is contained in:
commit
f77b10c8e2
10
README.md
10
README.md
@ -12,8 +12,11 @@ cd lib/sw_apps/zynq_fsbl/src/ && make BOARD=zed CFLAGS=-DFSBL_DEBUG_INFO
|
||||
Requirements [TBC]:
|
||||
- cmake
|
||||
- arm-none-eabi-gcc
|
||||
- doxygen
|
||||
- graphviz
|
||||
|
||||
|
||||
Configure doxygen:
|
||||
- export DOT_PATH=/usr/local/bin
|
||||
|
||||
satisfy Rust requirements
|
||||
```sh
|
||||
@ -21,11 +24,14 @@ cd ../mission_rust
|
||||
cargo update
|
||||
rustup toolchain install nightly
|
||||
rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu
|
||||
rustup override set default nightly
|
||||
rustup override set nightly
|
||||
cargo build -Z build-std
|
||||
```
|
||||
|
||||
In .../obsw/
|
||||
```sh
|
||||
git submodule init
|
||||
git submodule update
|
||||
mkdir build_cli
|
||||
cd build_cli
|
||||
cmake -DCMAKE_TOOLCHAIN_FILE=../bsp_z7/cmake/arm-none-eabi.toolchain ..
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
// TODO namespace the names
|
||||
|
||||
SemaphoreHandle_t * global_threading_semaphore = NULL;
|
||||
SemaphoreHandle_t global_threading_semaphore = NULL;
|
||||
|
||||
uint8_t global_threading_available_c() {
|
||||
if (global_threading_semaphore == NULL) {
|
||||
|
@ -7,19 +7,21 @@ As far as rust goes, static allocation and multithreading with a C RTOS is not p
|
||||
In a multithreaded software, references to data need to be passed to enable communication between objects, to be able to execute a task (which requires passing a reference to the task's data (which contains information on other tasks/data) to the RTOS), to send messages (passing the reference to the Queue, however encapsulated, is a reference to the queue which needs to be located somewhere) or to acess shared data (mutexes, same as with queues).
|
||||
All of these communication techniques are essential for this framework, so solutions need to be provided to be able to write safe code.
|
||||
|
||||
While statically allocating all (shared) data is generally possible and might be a possible solution to static allocation, it is not consistent with either the object oriented style of this framework, nor with general rust coding style (which discourages static data).
|
||||
While statically allocating all (shared) data, using global `'static` variables putting them into static memory instead of the stack, is generally possible and might be a possible solution to static allocation, it is not consistent with either the object oriented style of this framework, nor with general rust coding style (which discourages static data).
|
||||
|
||||
# The framework's approach
|
||||
|
||||
## Task Executor
|
||||
|
||||
Central element in running multithreaded is the task executor. It is borrowing references to all data to be used during runtime. This way, the references are guaranteed to be valid for the lifetime of the executor. By then coupling the availability of tasks to the executor (dropping the executor stops all tasks), the references can be guaranteed to be valid for the whole time tasks are available.
|
||||
Central element in running multithreaded is the task executor. It is borrowing references to all data to be used during runtime. This way, the references are guaranteed to be valid for the lifetime of the executor. This can be seen as 'pinning' the objects on the stack to fixed adresses.
|
||||
By then coupling the availability of tasks to the executor (dropping the executor stops all tasks), the references can be guaranteed to be valid for the whole time tasks are available.
|
||||
|
||||
The step where references to other objects is stored in structs is called initialization. This is (enforced by the compiler via layout of the corresponding functions) the only time where access to other objects is granted and references can be stored. The initialization is performed by the task executor as well, so at that time refereces are already fixed to their threading-time value.
|
||||
The step where references to other objects are stored in structs is called initialization. This is (enforced by the compiler via layout of the corresponding functions) the only time where access to other objects is granted and references can be stored.
|
||||
The initialization is performed by the task executor which controls that only pinned objects are given access to other pinned objects. Pinned objects are given an API to access other pinned objects (provided by an `ObjectManager`) as well as a token. The token can be passed to functions which will return references to objects to be stored by the calling object. This way, these references can only be created during the initialization.
|
||||
|
||||
For an production software, as soon as all tasks are started, the initial task is stopped/deleted. As such, there is no way (and no intention) for the multithreading to ever stop. In that case, the references shared during initialization will be valid for the whole runtime of the program.
|
||||
|
||||
As there might be use cases, for example in test cases, where multithreading is to be stopped, additional safeguards are implemented to ensure that the references shared during initialization are invalidated or their use is restricted. As running outside of the multithreaded environment is not meant for production, failing without corruption, ie panicking, is an acceptable way out. This is implemented by using a global semaphore indicating if the thread executor is alive. If that is not the case, all access to thared ressources will result in a panic. This adds an additional overhead to all access to shared data, namely checking the semaphore.
|
||||
As there might be use cases, for example in test cases, where multithreading is to be stopped, additional safeguards are implemented to ensure that the references shared during initialization are invalidated or their use is restricted. As running outside of the multithreaded environment is not meant for production, failing without corruption, ie panicking, is an acceptable way out. This is implemented by using a global semaphore indicating if the thread executor is alive. If that is not the case, all access to shared ressources will result in a panic. This adds an additional overhead to all access to shared data, namely checking the semaphore.
|
||||
|
||||
## Shared references
|
||||
|
||||
@ -29,9 +31,11 @@ The only time where references to other objects can be acquired is the initializ
|
||||
|
||||
Access to the other objects is granted via an object manager implementation, which will provide other objects as a nonmutable reference. Again, this reference can not be stored, only queried, without violating the borrow checker.
|
||||
|
||||
The reference obtained by the object manager is typed as `dyn SystemObjectIF`, so the ways to obtain references is governed by this trait and its super traits. These traits only offer threadsafe references, ie ones protected by RTOS primitives, to be obtained.
|
||||
The reference obtained by the object manager is typed as `dyn SystemObjectIF`, so the ways to obtain references is governed by this trait and its super traits. These traits only offer threadsafe APIs. Additionally, these APIs require a token which will be passed by the task executor so they can only be used during initialization.
|
||||
|
||||
Those references are either protected by a mutex or implemented using queues, which are the two primitives used to implement threadsafety. As such all access to shared references must use either the mutex or the queue API which is protected by an additional semaphore as described above.
|
||||
Those references are either protected by a mutex or implemented using queues, which are the two primitives used to implement thread safety. As such, all access to shared references must use either the mutex or the queue API which is protected by an additional semaphore as described above.
|
||||
|
||||
In some cases, smart pointers are used. These store raw pointers to other objects offering a threadsafe API. As dereferencing such a pointer is only allowed when objects are pinned, the dereferencing is protected by the additional semaphore as well.
|
||||
|
||||
## RTOS Metadata
|
||||
|
||||
@ -43,7 +47,7 @@ Two options exist for storing the descriptors. Either they are stored in a preal
|
||||
|
||||
The first option has the advantage that in C, the size of the descriptors is known at compile time (it is determined by the RTOS configuration which is implemented in C macros). Its disadvantage is that the size of the array needs to be adapted to the actual number ob instances used by the RTOS, which might not be trivially determined except for running the software.
|
||||
|
||||
The second option does not have this disadvantage, as the memory is provided by the user of the API. The disadvantage here is that the size of the data needs to be encoded manually for the selected configuration of the RTOS, which again can only be verified during runtime, when rust an C interact.
|
||||
The second option does not have this disadvantage, as the memory is provided by the user of the API. The disadvantage here is that the size of the data needs to be encoded manually for the selected configuration of the RTOS, which again can only be verified during runtime, when rust and C interact.
|
||||
|
||||
Both solutions lead to a detection of the configuration error (too few descriptors preallocated, too little memory allocated) only during runtime. As the configuration of the RTOS is expected to be more stable than the number of primitive instances, the second option is implemented.
|
||||
|
||||
@ -61,4 +65,14 @@ The handling of the failure is delegated to the structs using the primitives, as
|
||||
|
||||
* Uninitialized `MessageQueue`s will return empty from `read()` calls
|
||||
* Uninitialized `Mutex`es [TBCoded] will behave nominally, but not lock any actual mutex
|
||||
* Uninitialized `OwnedDataset`s will behave nominally, but not lock any mutex (which is uninitialized)
|
||||
* Uninitialized `OwnedDataset`s will behave nominally, but not lock any mutex (which is uninitialized)
|
||||
* Uninitialized `ReferencedDataset`s will return default data on `read()` and do nothing on `commit()`
|
||||
|
||||
## Smart pointers
|
||||
|
||||
Datasets are smart pointers to T, with a read/commit API. Both a mutex and the threading semaphore protect each call. Clones (`ReferencedDataset`) are created invalid and can only made valid during init using the init token.
|
||||
Can be compared to the mutex in rust std with a slightly different API.
|
||||
|
||||
Stores are a statically preallocated slotted allocation scheme:
|
||||
StoreAccessor is a smart pointer to the Store, offering same API as Store. Internally, it dereferences a raw pointer in each call, protected by the threading semaphore. Store in turn protects a shared backend with a mutex. Acessors can only be obtained during init. Together, StoreAccessor and Store are an thread safe allocator to the backend.
|
||||
StoreSlots are smart pointers to memory allocated in a store backend. Can be created/allocated during runtime.
|
@ -1,60 +1,18 @@
|
||||
use super::{mutex, objectmanager};
|
||||
use super::objectmanager::SystemObjectIF;
|
||||
use crate::check_global_threading_available;
|
||||
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
pub struct TypeId {
|
||||
id: usize,
|
||||
}
|
||||
|
||||
// inspired by https://github.com/jswrenn/deflect/
|
||||
pub trait HasTypeId {
|
||||
#[inline(never)]
|
||||
fn get_type_id(&self) -> TypeId {
|
||||
TypeId {
|
||||
id: Self::get_type_id as usize,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait DatapoolOwnerIF {
|
||||
fn get_set(&self, _type_id: TypeId) -> Option<&dyn DataSetIF> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub trait DataSetIF {
|
||||
fn get_type_id(&self) -> TypeId;
|
||||
fn get_actual_data(&self) -> &dyn HasTypeId;
|
||||
fn get_mutex(&self) -> mutex::RawMutex;
|
||||
}
|
||||
|
||||
pub struct OwnedDataset<T: HasTypeId + Copy> {
|
||||
pub struct OwnedDataset<T: Clone> {
|
||||
actual_data: T,
|
||||
mutex: mutex::RawMutex,
|
||||
}
|
||||
|
||||
pub struct ReferencedDataset<T: HasTypeId + Copy> {
|
||||
pub struct ReferencedDataset<T: Clone> {
|
||||
//we use a pointer here to avoid lifetimes
|
||||
actual_data: Option<*const T>,
|
||||
mutex: Option<mutex::RawMutex>,
|
||||
}
|
||||
|
||||
impl<T: HasTypeId + Copy + Default> DataSetIF for OwnedDataset<T> {
|
||||
fn get_type_id(&self) -> TypeId {
|
||||
self.actual_data.get_type_id()
|
||||
}
|
||||
|
||||
fn get_actual_data(&self) -> &dyn HasTypeId {
|
||||
&self.actual_data
|
||||
}
|
||||
|
||||
fn get_mutex(&self) -> mutex::RawMutex {
|
||||
self.initialize();
|
||||
return self.mutex;
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: HasTypeId + Copy + Default> OwnedDataset<T> {
|
||||
impl<T: Clone + Default> OwnedDataset<T> {
|
||||
pub fn new() -> OwnedDataset<T> {
|
||||
OwnedDataset::<T> {
|
||||
actual_data: T::default(),
|
||||
@ -62,12 +20,19 @@ impl<T: HasTypeId + Copy + Default> OwnedDataset<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_init_values(init_values: T) -> OwnedDataset<T> {
|
||||
OwnedDataset::<T> {
|
||||
actual_data: init_values,
|
||||
mutex: mutex::RawMutex::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(&mut self) -> Result<T, ()> {
|
||||
let _mutex_guard = match self.mutex.take() {
|
||||
Err(()) => return Err(()),
|
||||
Ok(guard) => guard,
|
||||
};
|
||||
Ok(self.actual_data)
|
||||
Ok(self.actual_data.clone())
|
||||
}
|
||||
|
||||
//TODO do we want to know if it fails?
|
||||
@ -78,13 +43,9 @@ impl<T: HasTypeId + Copy + Default> OwnedDataset<T> {
|
||||
};
|
||||
self.actual_data = data;
|
||||
}
|
||||
|
||||
fn initialize(&mut self) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: HasTypeId + Copy + Default> ReferencedDataset<T> {
|
||||
impl<T: Clone + Default + 'static> ReferencedDataset<T> {
|
||||
pub fn new() -> ReferencedDataset<T> {
|
||||
ReferencedDataset::<T> {
|
||||
actual_data: None,
|
||||
@ -93,37 +54,36 @@ impl<T: HasTypeId + Copy + Default> ReferencedDataset<T> {
|
||||
}
|
||||
|
||||
pub fn read(&mut self) -> Result<T, ()> {
|
||||
let _mutex_guard = match self.mutex {
|
||||
None => return Err(()),
|
||||
Some(mutex) => match mutex.take() {
|
||||
Err(()) => return Err(()),
|
||||
Ok(guard) => guard,
|
||||
},
|
||||
};
|
||||
match self.actual_data {
|
||||
None => Err(()),
|
||||
Some(data) => Ok(unsafe { *data }),
|
||||
}
|
||||
let mutex = self.mutex.as_ref().ok_or(())?;
|
||||
let _guard = mutex.take()?;
|
||||
let pointer = self.actual_data.ok_or(())?;
|
||||
|
||||
// we are only allowed to use the pointer during threading
|
||||
check_global_threading_available!();
|
||||
Ok(unsafe { (*pointer).clone() })
|
||||
}
|
||||
|
||||
//Note, passing the object_manager is per design, so that this call can only be used during init (when storing the address is valid)
|
||||
pub fn initialize(&mut self, object_manager: &dyn objectmanager::ObjectManager, owner_of_the_set: objectmanager::ObjectId) -> Result<(), ()> {
|
||||
pub fn initialize(
|
||||
&mut self,
|
||||
object_manager: &dyn objectmanager::ObjectManager,
|
||||
owner_of_the_set: objectmanager::ObjectId,
|
||||
) -> Result<(), ()> {
|
||||
let owner = object_manager.get_object(owner_of_the_set)?;
|
||||
let temp: T = T::default(); //TODO find nicer solution whithout local instance and trait bound to Default
|
||||
let type_id = temp.get_type_id();
|
||||
let other_set: &dyn DataSetIF;
|
||||
match owner.get_set(type_id) {
|
||||
None => {
|
||||
return Err(());
|
||||
}
|
||||
Some(set) => {
|
||||
other_set = set;
|
||||
}
|
||||
}
|
||||
//pointer cast is safe because we checked the type_id
|
||||
//getting pointer to avoid lifetime check
|
||||
self.actual_data = Some(other_set.get_actual_data() as *const dyn HasTypeId as *const T);
|
||||
self.mutex = Some(other_set.get_mutex());
|
||||
owner.for_each_member_return(&mut |x, _| {
|
||||
self.try_initialize_from(x.downcast_ref()?);
|
||||
None
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_initialize_from(&mut self, other: &OwnedDataset<T>) {
|
||||
// TODO check some id to allow the other to own multiple
|
||||
// instances of <T>. Otherwise, this will select the last
|
||||
// one.
|
||||
// if !self.id == other.id {
|
||||
// return
|
||||
// }
|
||||
self.actual_data = Some(&other.actual_data);
|
||||
self.mutex = Some(other.mutex.clone());
|
||||
}
|
||||
}
|
||||
|
63
mission_rust/src/fsrc/introspection.rs
Normal file
63
mission_rust/src/fsrc/introspection.rs
Normal file
@ -0,0 +1,63 @@
|
||||
/// A trait to allow objects to be queried for some of their inner attributes at runtime.
|
||||
///
|
||||
pub trait Introspection {
|
||||
/// Executes a function/closure on all members of the implementing struct.
|
||||
///
|
||||
/// The parameters for the closure a reference to the member as well as the name of the member.
|
||||
///
|
||||
/// To keep the trait object safe, we need to pass the closure as a reference. Otherwise,
|
||||
/// the trait can not be used as a `dyn` reference, because the function becomes generic.
|
||||
/// (Either using `impl FnMut` not `where F: FnMut` will be treated as generic making the
|
||||
/// trait not object safe).
|
||||
/// Additionally, the closure is typed as `FnMut`, to allow the closure to capture mutably.
|
||||
///
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use fsrc::introspection::Introspection;
|
||||
///
|
||||
/// struct Empty {}
|
||||
///
|
||||
/// impl Empty {
|
||||
/// fn getRandomNumber(&self) -> u8 {
|
||||
/// return 4 // chosen by fair dice roll.
|
||||
/// // guaranteed to be random.
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// fn parsing_something(something: &dyn Introspection) {
|
||||
/// let mut a = 0;
|
||||
/// something.for_each_member(&mut |x, _| {
|
||||
/// if let Some( empty) = x.downcast_ref::<Empty>() {
|
||||
/// a = empty.getRandomNumber();
|
||||
/// }
|
||||
/// });
|
||||
/// }
|
||||
/// ````
|
||||
///
|
||||
fn for_each_member(&self, f: &mut dyn FnMut(&dyn core::any::Any, &str) -> ());
|
||||
|
||||
/// Same as [for_each_member](Introspection::for_each_member), only with a return value for the closure.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// The return value allows using `x.downcast_ref()?` or similar constructs in the closure like in:
|
||||
///
|
||||
/// ```rust
|
||||
/// use fsrc::introspection::Introspection;
|
||||
///
|
||||
/// struct Empty {}
|
||||
///
|
||||
/// fn do_something(_: &Empty) {}
|
||||
///
|
||||
/// fn parsing_something(something: &dyn Introspection) {
|
||||
/// something.for_each_member_return(&mut |x, _| {
|
||||
/// do_something(x.downcast_ref()?);
|
||||
/// None
|
||||
/// });
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
fn for_each_member_return(&self, f: &mut dyn FnMut(&dyn core::any::Any, &str) -> Option<()>);
|
||||
}
|
@ -6,4 +6,6 @@ pub mod osal;
|
||||
pub mod tasks;
|
||||
pub mod objectmanager;
|
||||
pub mod datasets;
|
||||
mod mutex;
|
||||
pub mod store;
|
||||
mod mutex;
|
||||
pub mod introspection;
|
@ -1,7 +1,13 @@
|
||||
use crate::check_global_threading_available;
|
||||
|
||||
use super::osal;
|
||||
|
||||
// TODO we do not discern between a mutex (holding the descriptor) and a clone (pointing to the
|
||||
// original descriptor), waisting some memory
|
||||
|
||||
pub struct RawMutex {
|
||||
handle: Option<*const core::ffi::c_void>
|
||||
handle: Option<*const core::ffi::c_void>,
|
||||
descriptor: [u8; 10], //TODO which size?
|
||||
}
|
||||
|
||||
pub struct RawMutexGuard {
|
||||
@ -10,37 +16,59 @@ pub struct RawMutexGuard {
|
||||
|
||||
impl Drop for RawMutexGuard {
|
||||
fn drop(&mut self) {
|
||||
//TODO do we need some check here?
|
||||
unsafe{osal::give_mutex(self.handle)};
|
||||
check_global_threading_available!(); // tell me when you reach this one!
|
||||
// We use nullptr as marker for an uninitialized mutex, which we
|
||||
// must not give
|
||||
if self.handle != 0 as *const core::ffi::c_void {
|
||||
unsafe { osal::give_mutex(self.handle) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//TODO for non allocating, keep handle as option, to be set either in an initialize() call
|
||||
// or maybe lazy later on, TBD if lazy is needed or we can guarantee init to be called
|
||||
impl RawMutex {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
handle: None
|
||||
handle: None,
|
||||
descriptor: [0; 10],
|
||||
}
|
||||
}
|
||||
|
||||
fn initialize(&mut self) {
|
||||
// check_global_threading_available!(); we have a token, so this is redundant
|
||||
if self.handle != None {
|
||||
return;
|
||||
}
|
||||
// TODO verify handle size
|
||||
let handle = unsafe { osal::create_mutex() };
|
||||
if handle == 0 as *const core::ffi::c_void {
|
||||
panic!("Could not create mutex")
|
||||
}
|
||||
self.handle = Some(handle);
|
||||
}
|
||||
|
||||
pub fn take(&self) -> Result<RawMutexGuard, ()> {
|
||||
check_global_threading_available!();
|
||||
if let Some(handle) = self.handle {
|
||||
return match unsafe { osal::take_mutex(handle) } {
|
||||
1 => Ok(RawMutexGuard { handle: handle }),
|
||||
_ => Err(()), // Only when timeout expired (we have none) TODO error code
|
||||
};
|
||||
} else {
|
||||
// nullptr makes the guard do nothing
|
||||
Ok(RawMutexGuard {
|
||||
handle: 0 as *const core::ffi::c_void,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TODO protect with token
|
||||
pub fn clone(&self) -> Self {
|
||||
let mut_self = self as *const Self as *mut Self; //oh look, a C developer wrote this
|
||||
unsafe { (*mut_self).initialize() }; //TODO this might be safe (we are in init), but does not look very good
|
||||
// At this point self.handle must be valid, initialize would have panicked otherwise
|
||||
Self {
|
||||
handle: self.handle
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take(&self) -> Result<RawMutexGuard,()> {
|
||||
osal::check_global_threading_available();
|
||||
let handle = match self.handle {
|
||||
None => return Err(()), //TODO plan was that this failes silently -> implement an empty mutex guard?
|
||||
Some(handle) => handle
|
||||
};
|
||||
match unsafe {osal::take_mutex(handle)} {
|
||||
1 => Ok(RawMutexGuard { handle: handle }),
|
||||
_ => Err(()) //TODO error code
|
||||
handle: self.handle,
|
||||
descriptor: [0; 10],
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,12 +85,4 @@ impl RawMutex {
|
||||
// _ => Err(()) //TODO error code
|
||||
// }
|
||||
// }
|
||||
|
||||
pub fn initialize(&mut self) {
|
||||
let handle = unsafe {osal::create_mutex()};
|
||||
if handle == 0 as *const core::ffi::c_void {
|
||||
panic!("Could not create mutex")
|
||||
}
|
||||
self.handle = Some(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ pub trait ObjectManager<'a> {
|
||||
fn get_object(&self, id: ObjectId) -> Result<&'a dyn SystemObjectIF, ()>;
|
||||
}
|
||||
|
||||
pub trait SystemObjectIF: tasks::ExecutableObjectIF + datasets::DatapoolOwnerIF {
|
||||
pub trait SystemObjectIF: tasks::ExecutableObjectIF + introspection::Introspection {
|
||||
fn get_id(&self) -> ObjectId;
|
||||
fn initialize(&mut self, object_manager: &dyn ObjectManager) -> Result<(), ()>;
|
||||
fn get_command_queue(&self) -> queues::MessageQueueSender;
|
||||
|
@ -3,6 +3,17 @@ type TaskFunction = unsafe extern "C" fn(*mut core::ffi::c_void);
|
||||
//TODO verify uXX == uintXX_t
|
||||
//TODO safe API
|
||||
|
||||
|
||||
// This is a macro so that the panic is at the right place
|
||||
#[macro_export]
|
||||
macro_rules! check_global_threading_available {
|
||||
() => (
|
||||
if !crate::osal::global_threading_available() {
|
||||
panic!("using threaded API outside of threading environment")
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
pub fn outbyte(c: u8);
|
||||
//void *create_task(TaskFunction_t taskFunction, void *parameter, size_t stack_size)
|
||||
@ -37,20 +48,20 @@ extern "C" {
|
||||
|
||||
}
|
||||
|
||||
pub fn global_threading_available() -> bool {
|
||||
unsafe{global_threading_available_c() == 1}
|
||||
}
|
||||
|
||||
pub fn check_global_threading_available() {
|
||||
if !global_threading_available() {
|
||||
panic!("using queue outside of threading environment")
|
||||
pub fn task_delete_self() {
|
||||
unsafe {
|
||||
delete_task(0 as *const core::ffi::c_void);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enable_global_threading(){
|
||||
unsafe{enable_global_threading_c()};
|
||||
pub fn global_threading_available() -> bool {
|
||||
unsafe { global_threading_available_c() == 1 }
|
||||
}
|
||||
|
||||
pub fn disable_global_threading(){
|
||||
unsafe{disable_global_threading_c()};
|
||||
}
|
||||
pub fn enable_global_threading() {
|
||||
unsafe { enable_global_threading_c() };
|
||||
}
|
||||
|
||||
pub fn disable_global_threading() {
|
||||
unsafe { disable_global_threading_c() };
|
||||
}
|
||||
|
@ -1,4 +1,7 @@
|
||||
use crate::osal;
|
||||
use crate::{check_global_threading_available, osal};
|
||||
|
||||
|
||||
// TODO test dropping of droppable data within messages (no drop on sending, dropping when receiving, no dropping when forwarding in new message)
|
||||
|
||||
pub struct MessageQueue<const DEPTH: usize> {
|
||||
queue_data: [Message; DEPTH],
|
||||
@ -18,7 +21,7 @@ impl<const DEPTH: usize> MessageQueue<DEPTH> {
|
||||
}
|
||||
|
||||
pub fn receive(&self) -> Option<Message> {
|
||||
osal::check_global_threading_available();
|
||||
check_global_threading_available!();
|
||||
let actual_id = match self.queue_id {
|
||||
None => return None,
|
||||
Some(id) => id,
|
||||
@ -26,7 +29,11 @@ impl<const DEPTH: usize> MessageQueue<DEPTH> {
|
||||
let mut message: Message = Message::default();
|
||||
let res: u8;
|
||||
unsafe {
|
||||
//message = core::mem::MaybeUninit::zeroed().assume_init(); // We only return it if the queue received something
|
||||
// safe beacuse:
|
||||
// OS will write not more than length of message queue elements
|
||||
// queue was created with size_of::<Message> as length of message queue elements
|
||||
// in MessageQueue::initialize
|
||||
// making this pointer access safe as long as OS is holding the contract
|
||||
let message_pointer: *mut core::ffi::c_void =
|
||||
&mut message as *mut _ as *mut core::ffi::c_void;
|
||||
res = osal::queue_receive(actual_id, message_pointer);
|
||||
@ -39,6 +46,7 @@ impl<const DEPTH: usize> MessageQueue<DEPTH> {
|
||||
}
|
||||
|
||||
fn initialize(&mut self) {
|
||||
check_global_threading_available!();
|
||||
if self.queue_id != None {
|
||||
return;
|
||||
}
|
||||
@ -53,6 +61,7 @@ impl<const DEPTH: usize> MessageQueue<DEPTH> {
|
||||
}
|
||||
|
||||
pub fn get_sender(&self) -> MessageQueueSender {
|
||||
check_global_threading_available!();
|
||||
let mut_self = self as *const Self as *mut Self; //oh look, a C developer wrote this
|
||||
unsafe { (*mut_self).initialize() }; //TODO this might be safe (we are in init), but does not look very good
|
||||
let queue_id = self.queue_id.expect("Queue uninitialized");
|
||||
@ -67,14 +76,23 @@ impl MessageQueueSender {
|
||||
Self { queue_id: None }
|
||||
}
|
||||
|
||||
// TODO if error, message is dropped, is this intended or should we return the message back out?
|
||||
pub fn send(&self, message: Message) -> Result<(), ()> {
|
||||
osal::check_global_threading_available();
|
||||
check_global_threading_available!();
|
||||
let queue_id = match self.queue_id {
|
||||
None => return Err(()),
|
||||
Some(id) => id,
|
||||
};
|
||||
// we move ownership out to C (so must not drop it)
|
||||
// regaining ownership in receive() where it then will be dropped
|
||||
let message = core::mem::ManuallyDrop::new(message);
|
||||
let res: u8;
|
||||
unsafe {
|
||||
// safe because:
|
||||
// OS will read not more than length of message queue elements
|
||||
// queue was created with size_of::<Message> as length of message queue elements
|
||||
// in MessageQueue::initialize
|
||||
// making this pointer access safe as long as OS is holding the contract
|
||||
let message_pointer: *const core::ffi::c_void =
|
||||
&message as *const _ as *const core::ffi::c_void;
|
||||
res = osal::queue_send(queue_id, message_pointer);
|
||||
|
72
mission_rust/src/fsrc/store.rs
Normal file
72
mission_rust/src/fsrc/store.rs
Normal file
@ -0,0 +1,72 @@
|
||||
use core::slice::SliceIndex;
|
||||
|
||||
use crate::check_global_threading_available;
|
||||
|
||||
trait StoreBackend {
|
||||
fn free_slot(&mut self, length: usize) -> Option<*mut [u8]>;
|
||||
}
|
||||
|
||||
struct Store<'a> {
|
||||
mutex: crate::fsrc::mutex::RawMutex,
|
||||
backend: &'a mut dyn StoreBackend,
|
||||
}
|
||||
|
||||
impl<'a> Store<'a> {
|
||||
pub fn accessor(&mut self) -> StoreAccessor {
|
||||
check_global_threading_available!();
|
||||
// holy shit, this is really unsafe, even for us:
|
||||
// we cast away the lifetime
|
||||
// this is safe as we only do it in threading
|
||||
// and the Accessor will only use it while still in threading
|
||||
// (the accessor is a smart pointer refusing to be smart outside
|
||||
// of threading)
|
||||
let static_pointer = unsafe { core::mem::transmute::<*mut Store<'a>, *mut Store<'static>>(self) };
|
||||
StoreAccessor {
|
||||
store: static_pointer,
|
||||
}
|
||||
}
|
||||
|
||||
fn free_slot(&mut self, length: usize) -> Result<StoreSlot, ()> {
|
||||
if let Ok(_guard) = self.mutex.take() {
|
||||
if let Some(data) = self.backend.free_slot(length){
|
||||
Ok(StoreSlot { data: data })
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO needs to be able to be uninitialized as it should be usable as member
|
||||
// A smart pointer to a Store protecting insane (static) pointer usage
|
||||
// only works during threading, panics otherwise
|
||||
struct StoreAccessor {
|
||||
store: *mut Store<'static>,
|
||||
}
|
||||
|
||||
impl StoreAccessor {
|
||||
fn free_slot(&self, length: usize) -> Result<StoreSlot, ()> {
|
||||
check_global_threading_available!();
|
||||
unsafe { (*self.store).free_slot(length) }
|
||||
}
|
||||
}
|
||||
|
||||
struct StoreSlot {
|
||||
data: *mut [u8],
|
||||
}
|
||||
|
||||
impl StoreSlot {
|
||||
pub fn get<I>(&self, index: I) -> &mut [u8]
|
||||
where I: SliceIndex<u8>
|
||||
{
|
||||
check_global_threading_available!();
|
||||
unsafe { &mut (*self.data) }
|
||||
}
|
||||
|
||||
pub fn get_all(&self) -> &mut [u8] {
|
||||
check_global_threading_available!();
|
||||
unsafe { &mut (*self.data) }
|
||||
}
|
||||
}
|
@ -2,6 +2,8 @@ use core::slice;
|
||||
|
||||
use super::objectmanager::ObjectManager;
|
||||
|
||||
// TODO find a way to report uxTaskGetStackHighWaterMarks during runtime as TM?
|
||||
|
||||
#[no_mangle]
|
||||
extern "C" fn task_entry(task_object: *mut core::ffi::c_void) {
|
||||
let task: &mut dyn TaskIF;
|
||||
@ -89,11 +91,22 @@ pub struct TaskExecutor<'a> {
|
||||
}
|
||||
|
||||
impl<'a> TaskExecutor<'a> {
|
||||
pub fn init_and_run(&mut self) {
|
||||
//TODO unlock global multitasking mutex
|
||||
/// Initializes all tasks (and the objects contained within them)
|
||||
/// and starts them.
|
||||
///
|
||||
/// It also enables global threading, allowing threading APIs to be used (queues, mutexes etc)
|
||||
/// See TODO about threading
|
||||
///
|
||||
///# Arguments
|
||||
///
|
||||
/// * `delete_init_task` - If true, will delete the init task (the task this functions is called in) which means the function will not return
|
||||
///
|
||||
pub fn init_and_run(&mut self, delete_init_task: bool) {
|
||||
let object_manager = TaskObjectManager {
|
||||
tasks: unsafe { slice::from_raw_parts(self.tasks.as_ptr(), self.tasks.len()) },
|
||||
};
|
||||
// init uses unsafe methods and checks against the lock, so we need to enable it here
|
||||
crate::fsrc::osal::enable_global_threading();
|
||||
for task in self.tasks.iter_mut() {
|
||||
let _ = task.initialize(&object_manager).unwrap();
|
||||
}
|
||||
@ -101,14 +114,18 @@ impl<'a> TaskExecutor<'a> {
|
||||
for task in self.tasks.iter_mut() {
|
||||
// we give away a raw pointer, to be called by an OS task
|
||||
// while this is generally very broken, we use a reference tied
|
||||
// to our own lifetime and destroy the task when we get dropped
|
||||
// to our own lifetime and destroy the task when we get dropped.
|
||||
// this way, the reference is guaranteed to be valid over our
|
||||
// lifetime while the task is deleted at the end of our lifetime
|
||||
let task_pointer: *const core::ffi::c_void = *task as *mut _ as *const core::ffi::c_void; //TODO this does work without the "*" in front of the task -> Why??
|
||||
let task_pointer: *const core::ffi::c_void =
|
||||
*task as *mut _ as *const core::ffi::c_void; //TODO this does work without the "*" in front of the task -> Why??
|
||||
let handle;
|
||||
unsafe {
|
||||
handle =
|
||||
crate::fsrc::osal::create_task(task_entry, task_pointer, u32::try_from(task.get_stack_size()).unwrap());
|
||||
handle = crate::fsrc::osal::create_task(
|
||||
task_entry,
|
||||
task_pointer,
|
||||
u32::try_from(task.get_stack_size()).unwrap(),
|
||||
);
|
||||
}
|
||||
if handle == 0 as *mut core::ffi::c_void {
|
||||
panic!("could not create Task");
|
||||
@ -116,6 +133,9 @@ impl<'a> TaskExecutor<'a> {
|
||||
task.set_handle(handle);
|
||||
}
|
||||
}
|
||||
if delete_init_task {
|
||||
crate::osal::task_delete_self();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -141,9 +161,10 @@ impl<'a> crate::objectmanager::ObjectManager<'a> for TaskObjectManager<'a> {
|
||||
|
||||
impl<'a> Drop for TaskExecutor<'a> {
|
||||
fn drop(&mut self) {
|
||||
//TODO lock global multitasking mutex
|
||||
crate::fsrc::osal::disable_global_threading();
|
||||
for task in self.tasks.iter_mut() {
|
||||
unsafe {
|
||||
// TODO print uxTaskGetStackHighWaterMark() for each stack
|
||||
crate::fsrc::osal::delete_task(task.get_handle());
|
||||
}
|
||||
}
|
||||
|
@ -3,12 +3,11 @@
|
||||
//TODO os errors in API calls
|
||||
//TODO look into a pattern for late initialized stuff, currently using Option (can we make it compile time safe?)
|
||||
|
||||
mod fsrc;
|
||||
pub mod fsrc;
|
||||
|
||||
use core::fmt::Write;
|
||||
use core::panic::PanicInfo;
|
||||
|
||||
use fsrc::datasets::{DataSetIF, DatapoolOwnerIF};
|
||||
use fsrc::objectmanager::SystemObjectIF;
|
||||
use fsrc::*;
|
||||
|
||||
@ -49,18 +48,16 @@ extern "C" fn rust_main() {
|
||||
sifln!("Mission done");
|
||||
}
|
||||
|
||||
#[derive (Copy, Clone, Default)]
|
||||
#[derive(Copy, Clone, Default)]
|
||||
struct HandlerData {
|
||||
x: u32,
|
||||
y: f32
|
||||
y: f32,
|
||||
}
|
||||
|
||||
impl datasets::HasTypeId for HandlerData {}
|
||||
|
||||
struct Handler {
|
||||
id: objectmanager::ObjectId,
|
||||
command_queue: queues::MessageQueue<10>,
|
||||
data: datasets::OwnedDataset<HandlerData>
|
||||
data: datasets::OwnedDataset<HandlerData>,
|
||||
}
|
||||
|
||||
struct HandlerSender {
|
||||
@ -68,10 +65,9 @@ struct HandlerSender {
|
||||
other_handler: objectmanager::ObjectId,
|
||||
cycle: u8,
|
||||
other_handler_queue: queues::MessageQueueSender,
|
||||
other_data: datasets::ReferencedDataset<HandlerData>
|
||||
other_data: datasets::ReferencedDataset<HandlerData>,
|
||||
}
|
||||
|
||||
|
||||
impl Handler {
|
||||
fn handle_message(&self, message: queues::Message) {
|
||||
match message {
|
||||
@ -134,17 +130,23 @@ impl SystemObjectIF for Handler {
|
||||
}
|
||||
}
|
||||
|
||||
impl datasets::DatapoolOwnerIF for Handler {
|
||||
fn get_set(&self, type_id: datasets::TypeId) -> Option<&dyn datasets::DataSetIF>{
|
||||
if type_id == self.data.get_type_id(){
|
||||
Some(&self.data)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
impl introspection::Introspection for HandlerSender {
|
||||
fn for_each_member(&self, f: &mut dyn FnMut(&dyn core::any::Any, &str) -> ()) {}
|
||||
|
||||
fn for_each_member_return(&self, f: &mut dyn FnMut(&dyn core::any::Any, &str) -> Option<()>) {}
|
||||
}
|
||||
|
||||
impl DatapoolOwnerIF for HandlerSender {}
|
||||
impl introspection::Introspection for Handler {
|
||||
fn for_each_member(&self, f: &mut dyn FnMut(&dyn core::any::Any, &str) -> ()) {
|
||||
(*f)(&self.command_queue, "command_queue");
|
||||
(*f)(&self.data, "data");
|
||||
}
|
||||
|
||||
fn for_each_member_return(&self, f: &mut dyn FnMut(&dyn core::any::Any, &str) -> Option<()>) {
|
||||
(*f)(&self.command_queue, "command_queue");
|
||||
(*f)(&self.data, "data");
|
||||
}
|
||||
}
|
||||
|
||||
impl SystemObjectIF for HandlerSender {
|
||||
fn get_command_queue(&self) -> crate::fsrc::queues::MessageQueueSender {
|
||||
@ -156,14 +158,14 @@ impl SystemObjectIF for HandlerSender {
|
||||
}
|
||||
fn initialize(&mut self, object_manager: &dyn objectmanager::ObjectManager) -> Result<(), ()> {
|
||||
let other_handler_maybe = object_manager.get_object(self.other_handler);
|
||||
let other_handler =
|
||||
match other_handler_maybe {
|
||||
let other_handler = match other_handler_maybe {
|
||||
Ok(other) => other,
|
||||
Err(_) => return Err(()),
|
||||
};
|
||||
self.other_handler_queue = other_handler.get_command_queue();
|
||||
|
||||
self.other_data.initialize(object_manager, self.other_handler)?;
|
||||
self.other_data
|
||||
.initialize(object_manager, self.other_handler)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -174,14 +176,14 @@ fn mission() {
|
||||
let mut h1 = Handler {
|
||||
id: 1,
|
||||
command_queue: queues::MessageQueue::new(),
|
||||
data: datasets::OwnedDataset::new()
|
||||
data: datasets::OwnedDataset::new(),
|
||||
};
|
||||
let mut h2 = HandlerSender {
|
||||
id: 2,
|
||||
other_handler: 3,
|
||||
cycle: 0,
|
||||
other_handler_queue: queues::MessageQueueSender::new(),
|
||||
other_data: datasets::ReferencedDataset::new()
|
||||
other_data: datasets::ReferencedDataset::new(),
|
||||
};
|
||||
|
||||
let array: &mut [&mut dyn objectmanager::SystemObjectIF] = &mut [&mut h1];
|
||||
@ -199,7 +201,7 @@ fn mission() {
|
||||
tasks: &mut [&mut t1, &mut t2],
|
||||
};
|
||||
|
||||
task_executor.init_and_run();
|
||||
task_executor.init_and_run(false);
|
||||
|
||||
sifln!("Mission delay");
|
||||
unsafe {
|
||||
|
Loading…
x
Reference in New Issue
Block a user