realsense_rust/docs/architecture.rs
1//! # Realsense-Rust: Architecture & Guiding Principles
2//!
3//! At a high-level, the library tries to map the fundamental abstractions in the librealsense2 C-API, but to do so in a
4//! type-safe, Rust-native way. What this means is that we try to avoid unsafe methods where possible, and attempt to wrap
5//! some of the low-level abstractions from realsense-sys in types that can encapsulate the underlying unsafe pointers and
6//! values.
7//!
8//! Where possible, we try to follow a few guiding principles with regards to the library design. These are:
9//!
10//! 1. Prefer Rust-native types to types used through the FFI
11//! 2. Make lifetimes obvious for all data through the system.
12//! 3. Make invalid states unrepresentable (where possible).
13//! 4. Make error cases explicit and differentiable.
14//!
15//! We attempt to explain each of these below.
16//!
17//! ## Prefer Rust-native types to types used through the FFI
18//!
19//! In general, many of the people writing Rust code are doing so to maintain safety and efficiency. It is unnatural to have
20//! to think about types and semantics from other languages while writing code in Rust. The foreign function interface (FFI)
21//! unfortunately is one situation in which we must take care to do so.
22//!
23//! Realsense-rust aims to provide safe abstractions so that you can access functionality from librealsense2, but write your
24//! code in idiomatic Rust. What this means is that we avoid using or returning abstractions directly from the `bindgen`
25//! output generated by realsense-sys. There are several strategies that we take to try and avoid exposing lower level
26//! abstractions.
27//!
28//! ### Transform `std::os::raw` types to Rust
29//!
30//! The first of which is that we wrap all C-style enums in Rust enums. This is done using the
31//! [`num_derive`](https://crates.io/crates/num-derive) and [`num_traits`](https://crates.io/crates/num-traits) crates.
32//! Since all of the C-style enums in librealsense2 are bound as `u32` values with `bindgen`, we do a fairly small
33//! transformation back and forth between the two representations. C-style "strings" (read: `const char*`) are likewise
34//! converted into the Rust native [`std::ffi::CStr`](std::ffi::CStr).
35//!
36//! ### Provide concrete types rather than "extensible" pointers
37//!
38//! The second way in which we try to express idiomatic Rust requires some understanding of the underlying librealsense2
39//! library. In librealsense2, many types in the C-API are represented as pointers to opaque structs. However, many of
40//! the "types" are obscured, as these pointers have the ability to be "extended" to support certain interfaces. This is
41//! masking some of the inheritance based structure from the underlying C++ code that is the basis of librealsense2. In
42//! any case, extensions to frames, sensors, filters, etc. are all made possible through the `rs2_extension` enumeration
43//! (See [`Rs2Extension`](crate::kind::Rs2Extension) for how we handle this on the Rust side). What's unfortunate
44//! here is that all these extensions are contained within a single enumeration, as opposed to having a separate
45//! enumeration for frames, sensors, etc. This is awkward to use when actually programming, as the natural way to know
46//! exactly what type you have is not only to know what pointer type you received, but to ask the API if you can extend
47//! that pointer to a (growing) list of different extensions, many of which make no sense (e.g. you can never extend a
48//! frame to `rs2_extension_RS2_EXTENSION_ZERO_ORDER_FILTER`).
49//!
50//! Instead, we try to preemptively understand what concrete "type" of data you have from the pointers upfront, and make
51//! that clear in the Rust API by providing a concrete type back. This is why there are multiple structs for `VideoFrame`,
52//! `PoseFrame`, `MotionFrame`, etc. even though they all store a `*mut rs2_frame` to interface with the FFI's frame
53//! functionality underneath the hood. So in short: we do some preemptive checking of the types where necessary and produce
54//! concrete Rust types to represent them, even if in librealsense2 these types would be represented by the same pointer or
55//! opaque struct.
56//!
57//! ### Use vectors or native Rust containers over librealsense2 abstractions
58//!
59//! The third way in which types are kept Rusty is that for many types that would be expressed by a "list" (e.g.
60//! `rs2_device_list`, `rs2_stream_profile_list`) in the librealsense2 C-API are provided back as standard Rust vectors (and
61//! we take care of ownership / memory safety under the hood). This allows the use of all the things that vectors provide,
62//! rather than making our own managed list types.
63//!
64//! ## Errors
65//!
66//! The last thing we do with regards to keeping types Rusty concerns how we handle error types. More on this is written
67//! below.
68//!
69//! ### Make lifetimes obvious for all data through the system
70//!
71//! The librealsense2 C-API does not always do the best job at explaining object lifetimes. It is important to understand
72//! that librealsense2 is first and foremost implemented in C++, and the Rust wrapper we provide here is built on a
73//! C-wrapper around that C++ API. While the underlying C++ library takes advantage of C++11 abstractions such as
74//! `shared_ptr` or `unique_ptr` to declare ownership semantics, the C wrapper around it cannot express these types. So
75//! instead it uses raw pointers and attempts to use documentation to help close the gap between what pointers are managed
76//! vs. which are not.
77//!
78//! For users of realsense-rust, you should not have to think about this. The documentation for the C-API does not always
79//! describe what the exact lifetimes of the underlying types are. For this, the authors of this crate had to on occasion
80//! read the librealsense2 source to understand some of the ownership semantics. Where possible, we try to guarantee the
81//! lifetimes of data throughout the system, either by implementing the Drop trait directly, managing creation and deletion
82//! of pointers explicitly, or limiting the number of ways in which objects can be constructed (so as to prevent scenarios
83//! where lifetimes are sometimes managed or sometimes not).
84//!
85//! In most scenarios we aim to avoid making lifetimes explicit, but there are instances where that is not possible.
86//! However, one should expect that anything obtained from the high-level API is safe to retain unless otherwise noted.
87//! Please [submit a bug report](https://gitlab.com/tangram-vision-oss/realsense-rust/-/issues) if you've found some
88//! scenario in which an object you retained is holding onto invalid or otherwise deleted pointers.
89//!
90//! ### Make invalid states unrepresentable
91//!
92//! The main way we aim to do this is by understanding the lifetimes of the low-level pointers that the realsense-sys
93//! library returns (described above). However, many of the types in the system (especially frame types) will preemptively
94//! cache some data available through other interfaces ahead of time. A key example might be the `ImageFrame` struct, which
95//! caches width, height, stride, and the stream profile associated with the frame on construction. The reason this is done
96//! is because error handling with the C-API is awkward.
97//!
98//! Why is it awkward? Well, recall that librealsense2 is actually implemented in terms of C++. The library utilizes C++
99//! exceptions to signal errors. The C-API cannot do that as C has no way of expressing an exception (language doesn't
100//! support it). There are relatively few guarantees you can make about code that can signal exceptions, and so the vast
101//! majority of the C-API gets around this by capturing exceptions at the levels they can occur, and then wrapping the
102//! exception information in a pointer to an opaque type (`*mut rs2_error`). Of course, since exceptions can occur in so
103//! many places, almost every C-API function takes a `*mut *mut rs2_error`. Most of these checks are null-checks on the
104//! input pointer type, but not all.
105//!
106//! On the Rust side, we catch / check these `*mut rs2_error` types internally, and then signal this back to the user by
107//! returning a `Result` value of some kind. We cache some of the metadata or small fields in our Rust structs so that we
108//! can reduce the amount of `Result` checks that need to be done by the user, and likewise to keep relevant data cached as
109//! long as possible.
110//!
111//! One example where we need to keep "relevant data cached as long as possible" is in the frame types, specifically image
112//! frames. In order to be able to interpret the underlying data, we need to get the stream format, which is obtained by the
113//! stream profile. If we use the natural API to get this (i.e. `rs2_get_frame_stream_profile`), we can get the stream
114//! profile. However, this stream profile is managed by the device, so if the device is disconnected before the frame is
115//! processed, this pointer is no longer valid. We cache the stream format when constructing our own `StreamProfile` type on
116//! the Rust side so that even if the device is disconnected mid-way through streaming, you can still interpret the format
117//! and pixel data of your frame type.
118//!
119//! Since the frame stream profile is not owned by the frame (shared ownership managed through a device), we wouldn't
120//! otherwise be able to guarantee that the stream profile is available if the device the frame was streamed from was
121//! disconnected. This is a failure of the C-API. However, we manage to make invalid states unrepresentable since we do own
122//! the frame, and we can still access that data since we cache some of the small metadata (in this case, the format) on the
123//! Rust side.
124//!
125//! We do not copy / cache the underlying data from frames, as for e.g. a 720p image that involves a lot of copying and
126//! allocation, which is expensive and detrimental to users who want to build applications on top of realsense-rust while
127//! not sacrificing the speed or efficiency of the C or C++ librealsense2 APIs.
128//!
129//! ### Make error cases explicit and differentiable
130//!
131//! In cases where you might get an error from the low-level API you'll find that the high-level Rust wrapper provided by
132//! realsense-rust will return a `Result` of some kind. We do not shy away from making new types to express different
133//! classes of errors. However, given that the error information provided by the librealsense2 API is somewhat limited,
134//! you'll find most of our error types are of the form:
135//!
136//! ```no_run
137//! use realsense_rust::kind::Rs2Exception;
138//!
139//! pub enum SomeError { CouldNotXXX(Rs2Exception, String), }
140//! ```
141//!
142//! The enum field names should inform you what specific part of the function failed (if there are multiple parts). The
143//! internal [`Rs2Exception`](crate::kind::Rs2Exception) should inform you what category of exception was returned from the
144//! underlying API. The internal `String` is the exception message from librealsense2. If you find yourself hitting the same
145//! message often, this is a bug, and we would love if you [submitted a bug
146//! report](https://gitlab.com/tangram-vision-oss/realsense-rust/-/issues).