This document describes the high-level architecture of IronRDP.
Roughly, it takes 2x more time to write a patch if you are unfamiliar with the project, but it takes 10x more time to figure out where you should change the code.
This section talks briefly about various important directories and data structures.
Note also which crates are API Boundaries. Remember, rules at the boundary are different.
Set of foundational libraries for which strict quality standards must be observed. Note that all crates in this tier are API Boundaries. Pay attention to the "Architecture Invariant" sections.
Architectural Invariant: doing I/O is not allowed for these crates.
Architectural Invariant: all these crates must be fuzzed.
Architectural Invariant: must be #[no_std]
-compatible (optionally using the alloc
crate). Usage of the standard
library must be opt-in through a feature flag called std
that is enabled by default. When the alloc
crate is optional,
a feature flag called alloc
must exist to enable its use.
Architectural Invariant: no platform-dependant code (#[cfg(windows)]
and such).
Architectural Invariant: no non-essential dependency is allowed.
Architectural Invariant: no proc-macro dependency. Dependencies such as syn
should be pushed
as far as possible from the foundational crates so it doesn’t become too much of a compilation
bottleneck. Compilation time is a multiplier for everything.
The paper Developer Productivity For Humans, Part 4: Build Latency, Predictability,
and Developer Productivity by Ciera Jaspan and Collin Green, Google
researchers, also elaborates on why it is important to keep build times low.
Architectural Invariant: unless the performance, usability or ergonomic gain is really worth it, the amount of monomorphization incured in downstream user code should be minimal to avoid binary bloating and to keep the compilation as parallel as possible. Large generic functions should be avoided if possible.
Meta crate re-exporting important crates.
Architectural Invariant: this crate re-exports other crates and does not provide anything else.
Common traits and types.
This crate is motivated by the fact that only a few items are required to build most of the other crates such as the virtual channels.
To move up these crates up in the compilation tree, ironrdp-core
must remain small, with very few dependencies.
It contains the most "low-context" building blocks.
Most notable traits are Decode
and Encode
which are used to define a common interface for PDU encoding and decoding.
These are object-safe, and must remain so.
Most notable types are ReadCursor
, WriteCursor
and WriteBuf
which are used pervasively for encoding and decoding in a no-std
manner.
PDU encoding and decoding.
TODO: clean up the dependencies
Image processing primitives.
TODO: break down into multiple smaller crates
TODO: clean up the dependencies
Traits to implement RDP static virtual channels.
DRDYNVC static channel implementation and traits to implement dynamic virtual channels.
CLIPRDR static channel for clipboard implemented as described in MS-RDPECLIP.
RDPDR channel implementation.
RDPSND static channel for audio output implemented as described in MS-RDPEA.
State machines to drive an RDP connection sequence.
State machines to drive an RDP session.
Utilities to manage and build input packets.
RDCleanPath PDU structure used by IronRDP web client and Devolutions Gateway.
Lightweight and no_std
-compatible generic Error
and Report
types.
The Error
type wraps a custom consumer-defined type for domain-specific details (such as PduErrorKind
).
Higher level libraries and binaries built on top of the core tier. Guidelines and constraints are relaxed to some extent.
Blocking I/O abstraction wrapping the state machines conveniently.
This crate is an API Boundary.
Provides Future
s wrapping the state machines conveniently.
This crate is an API Boundary.
Framed*
traits implementation above tokio
’s traits.
This crate is an API Boundary.
Framed*
traits implementation above futures
’s traits.
This crate is an API Boundary.
TLS boilerplate common with most IronRDP clients.
NOTE: it’s not yet clear if this crate is an API Boundary or an implementation detail for the native clients.
Portable RDP client without GPU acceleration.
WebAssembly high-level bindings targeting web browsers.
This crate is an API Boundary (WASM module).
Core frontend UI used by iron-svelte-client
as a Web Component.
This crate is an API Boundary.
Web-based frontend using Svelte
and Material
frameworks.
Native CLIPRDR backend implementations.
Crates that are only used inside the IronRDP project, not meant to be published. This is mostly test case generators, fuzzing oracles, build tools, and so on.
Architecture Invariant: these crates are not, and will never be, an API Boundary.
proptest
generators for ironrdp-pdu
types.
proptest
generators for ironrdp-session
types.
Contains all integration tests for code living in the core tier, in a single binary, organized in modules.
Architectural Invariant: no dependency from another tier is allowed. It must be the case that compiling and running the core test suite does not require building any library from the extra tier. This is to keep iteration time short.
Contains all integration tests for code living in the extra tier, in a single binary, organized in modules.
(WIP: this crate does not exist yet.)
Provides test case generators and oracles for use with fuzzing.
Fuzz targets for code in core tier.
IronRDP’s free-form automation using Rust code.
Crates provided and maintained by the community. Core maintainers will not invest a lot of time into these. One or several community maintainers are associated to each one.
The IronRDP team is happy to accept new crates but may not necessarily commit to keeping them working when changing foundational libraries. We promise to notify you if such a crate breaks, and will always try to fix things when it's a minor change.
crates/ironrdp-acceptor
(@mihneabuz)
State machines to drive an RDP connection acceptance sequence
crates/ironrdp-server
(@mihneabuz)
Extendable skeleton for implementing custom RDP servers.
crates/ironrdp-glutin-renderer
(no maintainer)
glutin
primitives for OpenGL rendering.
crates/ironrdp-client-glutin
(no maintainer)
GPU-accelerated RDP client using glutin.
crates/ironrdp-replay-client
(no maintainer)
Utility tool to replay RDP graphics pipeline for debugging purposes.
This section talks about the things which are everywhere and nowhere in particular.
- Dependency injection when runtime information is necessary in core tier crates (no system call such as
gethostname
) - Keep non-portable code out of core tier crates
- Make crate
no_std
-compatible wherever possible - Facilitate fuzzing
- In libraries, provide concrete error types either hand-crafted or using
thiserror
crate - In binaries, use the convenient catch-all error type
anyhow::Error
- Free-form automation a-la
make
followingcargo xtask
specification
Architecture Invariant: core tier crates must never interact with the outside world. Only extra tier crates
such as ironrdp-client
, ironrdp-web
or ironrdp-async
are allowed to do I/O.
We use GitHub action and our workflows simply run cargo xtask
.
The expectation is that, if cargo xtask ci
passes locally, the CI will be green as well.
Architecture Invariant: cargo xtask ci
and CI workflow must be logically equivalents. It must
be the case that a successful cargo xtask ci
run implies a successful CI workflow run and vice versa.
We should focus on testing the public API of libraries (keyword: API boundary).
That’s why most (if not all) tests should go into the ironrdp-testsuite-core
and ironrdp-testsuite-extra
crates.
Architecture Invariant: tests do not depend on any kind of external resources, they are perfectly reproducible.
See fuzz/README.md
.
Do not include huge binary chunks directly in source files (*.rs
). Place these in separate files (*.bin
, *.bmp
)
and include them using macros such as include_bytes!
or include_str!
.
When comparing structured data (e.g.: error results, decoded PDUs), use expect-test
. It is both easy to create
and maintain such tests. When something affecting the representation is changed, simply run the test again with
UPDATE_EXPECT=1
env variable to magically update the code.
See:
- https://matklad.github.io/2021/05/31/how-to-test.html#Expect-Tests
- https://docs.rs/expect-test/latest/expect_test/
TODO: take further inspiration from rust-analyzer
- https://github.com/rust-lang/rust-analyzer/blob/d7c99931d05e3723d878bea5dc26766791fa4e69/docs/dev/architecture.md#testing
- https://matklad.github.io/2021/05/31/how-to-test.html
When a test can be generalized for multiple inputs, use rstest
to avoid code duplication.
It allows to test that certain properties of your code hold for arbitrary inputs, and if a failure is found, automatically finds the minimal test case to reproduce the problem.