Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a design/goals document to detail guidelines for the project #99

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# API Design guidelines for the LV2 Rust crate

* Memory Safety
* C Parity
* Real-Time Safety
* Performance
* Extensibility and Modularity
* Ergonomics and Usability
* Correctness
* Ecosystem integration
* `#![no_std]` compatibility

## Memory safety

### Goal: Exposing Safe and Sound LV2 APIs

### Anti-Goal: Exposing *only* Safe LV2 APIs

### Anti-Goal: Making Plugins robust against incorrect Host implementations

### Anti-Goal: Making Plugins robust against incorrect description (`.ttl`) files

## C Parity

### Goal: LV2 Users must be able to program the same behavior in both C and Rust

### Goal: Making every official LV2 API accessible through the `lv2` crate

### Anti-Goal: Making every other LV2 API accessible through the `lv2` crate

## Real-Time Safety

### Goal: Making all APIs needed for processing Real-Time Safe

### Nice-to-have: Making as many APIs as possible Real-Time Safe

### Non-Goal: Making Real-Time Safe API wrappers for non-Real-Time Safe LV2 APIs

### Anti-Goal: Enforcing Real-Time Safety in user code

## Performance

### Goal: Making APIs needed for processing as *blazingly fast* as possible

### Nice-to-have: Making APIs needed for processing as lightweight as possible

### Nice-to-have: Making all APIs as fast and lightweight as possible

## Extensibility and Modularity

### Goal: Make every LV2 spec into a separate crate

### Goal: Enable implementing new LV2 specifications on top of `lv2_core`

### Goal: Make every extensible LV2 spec extensible by other crates

### Goal: Use Rust `features` to disable optional external dependencies (if any)

### Future Goal: Split host-specific features using a `host` feature

## Ergonomics and Usability

### Nice-to-have: Follow the Rust API Guidelines and design "Rusty" APIs

### Non-goal: Design APIs to be as user-friendly as possible

## Correctness

### Nice-to-have: Design misuse-resistant APIs

### Anti-goal: Design all APIs to be impossible to misuse

## Ecosystem integration

### Goal: Integrate tightly with `core` and `std` standard libraries

### Nice-to-have: Provide integrations with popular and/or appropriate Rust libraries

## `#![no_std]` compatibility

### Future Goal: Make lv2_core `#![no_std]` compatible

### Nice-to-have: Make as many specs as `#![no_std]` compatible as possible

233 changes: 233 additions & 0 deletions GOALS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
# Goals for the Rust `lv2` crate

The `lv2` crate aims to enable Rust developers to use [LV2](https://lv2plug.in/) to create plugins
(and later, hosts), by providing them with a set of safe, idiomatic, extensible, and powerful APIs.

This document details the reasoning for those goals, as well as a few more. These major goals directly
dictate our [API design guidelines](./DESIGN.md), as they tell which users we direct this library towards.

## Making a Safe library

Safety is at the core of Rust's language and API design. If you don't need safety (mainly, memory and thread safety,
among others), then using Rust probably doesn't make sense for you, making this library irrelevant to your needs.

This library follows Rust's [official definition of Safety and Unsafety](https://doc.rust-lang.org/nomicon/meet-safe-and-unsafe.html),
as detailed in the [Rustonomicon](https://doc.rust-lang.org/nomicon/).

In short, APIs provided by this library that are not marked `unsafe` cannot, under any circumstance, trigger Undefined
Behavior. This is even more important in the context of real-time processing: incoherent output, or the whole system
going down, can have terrible consequences.

Safe APIs that can trigger Undefined Behavior are *unsound*, and we consider these bugs: they are either implementation
bugs or full API design flaws. These are marked with the `Unsound API` tag on GitHub, for better visibility.

**Note:** *While this library is in an alpha (`0.x`) stage, we might allow some soundness issues to stay for a bit, as
long as they are odd edge-case and not easy to trigger unwillingly. This is because the APIs are not complete
and still subject to change, however we will have to resolve all of these before getting to `1.0`.*

However, this library can provide APIs that are marked `unsafe`, if there are performance considerations in mind, or if
those are building blocks for higher levels of abstractions (such as Port Types). In such case, the whole API can be
made `unsafe`, if appropriate.

The main goal is for end-users to have to write as little `unsafe` code as possible by using this library, ideally none.

This library, however, does not try to be resilient against incorrect LV2 host implementations. Aside from checking
pointers to be not-null, and checking array indexing to be in-bounds, there is little that a plugin can do. Even when
a plugin can detect it, there is little it can do to alert the user something is wrong, other than printing to stderr
and going silent.

Therefore, we need to fully and blindly trust that all the data given to the plugin from the host is correct.

One exception to this rule are the manifest files (the `.ttl` files distributed alongside the plugin binaries). They
are usually user-written, and writing them incorrectly is likely to trigger Undefined Behavior, in both the plugin and
the host.

Higher-level, more specialized libraries and frameworks that rely on LV2 should probably auto-generate those files based on
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would consider what other crates do to be out-of-scope of this document. I would write "Higher-level libraries and frameworks that rely on LV2 can probably auto-generate those files based on plugin code and metadata."

plugin code and metadata. However, for the low-level control that `lv2` provides, these files need to be user-written
for now. It is possible that APIs relying on the content of those files could be marked `unsafe` in the future, even
if that makes it less practical to use for direct users of the `lv2` crates.

## No restrictions coming from C

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Supporting every single possibility of the LV2 specification is a nice goal. I would also like to take into account that we probably don't have a lot of (developer) resources to build and maintain this crate. So I think it would be good to prioritize features based on demand, host support and "contributor eagerness". Also, I'm not sure if we really need everything before version 1.0. E.g. deprecated specifications like Event are something I would not put any effort in. Also, the LV2 specification is a moving target, so it may not be feasible to have every feature implemented and to keep this crate on par with the LV2 specification.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes, I absolutely agree there. I should make this more clear, but this is more of a vision/design goals document, not a roadmap or prioritization document.

This means that while I probably won't push for the less-demanded (or supported) features before a little while (read: some years), I will still accept contributions for them.

Deprecated specifications should probably be implemented at least for hosts (when we support them properly), if they want to be compatible with older plugins. However, I agree this is not really a priority for now.

I agree that ideally, we should only need a viable library before reaching 1.0, and we could add the remaining specs on top of it afterwards. However, I've many times had the experience where adding an extra spec breaks many invariants and assumptions that other specs rely on, and require making breaking changes.

For instance, implementing Option requires a refactor of Atom to be actually usable, Instance Access and Data Access break both the single-threadedness and exclusivity invariants of lv2_core (basically requiring plugins to be Sync instead of just Send, and preventing any form of &mut reference to ever exist on a plugin instance), and implementing UI will probably break something that I haven't seen coming yet. 🙃

I will add those clarifications to the document. 🙂

While LV2 is most used to make digital synthesizers and MIDI or Audio processing plugins, it can do a lot of other
things.

LV2 can do [weird](https://lv2plug.in/ns/ext/morph) things. [Very weird](https://lv2plug.in/ns/ext/dynmanifest) things.

As long as they can be implemented safely, this library's goal is to expose every single possibility the LV2 specifications
provide. (The `lv2` crate can implement a few unsafe APIs for better, lower-level control, but if you need full-unsafe,
C-low-level control, the `lv2-sys` crate can always be used directly, but only for extreme edge-cases.)

The core idea is to make sure that no user of the `lv2` crate could stumble upon a case where an API is too restrictive
compared to the official LV2 spec.

See also the next section about this being a low-level library.

**Note:** *This library is in an alpha (`0.x`) stage right now. While the goal for `1.0` is to have every official
specification fully implemented, there might be a lot of missing specs or functionalities until then.*

## Low-level library

We want to expose as many LV2 APIs as possible while giving as much control as possible to the user. This may expose
tricky low-level details that DSP and UI developers shouldn't need to worry about (such as URIDs or Atoms).

However, because the `lv2` crate provides lots of flexibility for the user, it is easy to make more restrictive,
but easier to use high-level abstractions on top of this crate. Or at least, it is easier than trying to poke holes
through a high-level abstraction for some users that may need extra flexibility. Instead, those users can ditch the
abstractions (or parts of it) and use the low-level `lv2` library.
prokopyl marked this conversation as resolved.
Show resolved Hide resolved

Having a single, well-integrated low-level library also helps to lay down a solid foundation for LV2 in the ecosystem.
This allows all the complexity (and unsafety) of LV2 internals to be abstracted and shared among all Rust LV2 users.

This means that this library isn't ultimately designed to be the best possible development experience for plugin
authors. While plugins can be written directly on top of the `lv2` crate, users should expect having to handle
low-level details that can come in their way. There are also many, sometimes complex, abstractions that this crate
exposes, whose main purpose is to hide the pointer-juggly type twisting LV2 requires doing.

However, we do really appreciate plugin authors that choose to help to battle-test this crate by writing plugins on
top of it! Thank you very much! <3

## Idiomatic library

Just because we are writing a low-level wrapper for a complex C API, doesn't mean we can't expose a nice, idiomatic
Rust library when we can.

The [API Design Guidelines](./DESIGN.md) covers this in more detail, but the main goal is to integrate as closely
as possible with the Rust standard library, using types and traits such as `Result`, `Iterator`, `Debug`, `Error`, and
many more.

We also want to provide a good integration with other crates from the Rust ecosystem, as long as they are locked behind
optional, non-default [Cargo features](https://doc.rust-lang.org/cargo/reference/features.html). This way the user can
opt-in on better integration with the (sub-)ecosystem of their choosing, while keeping a minimal dependency tree.

Examples of crates that can be good to integrate with are `serde`, `wmidi`, or `baseview`, but there can be many more.

## Modularity and extensibility

The LV2 API is, by design, modular and extensible: only the minimal [LV2 Core specification]() is actually
required to make a working LV2 plugin. Everything else is a separate specification (and a separate header file) that
is built on top of it.

The `lv2` crate enforces this by having every single LV2 specification implemented in a separate sub-crate. In fact,
the `lv2` crate itself is nothing but re-exports of the sub-crates, each in a separate module gated by
[Cargo features](https://doc.rust-lang.org/cargo/reference/features.html). The `lv2` crate, in itself, is designed to be
nothing but a nice landing point for users.

For instance, the [LV2 MIDI specification](http://lv2plug.in/ns/ext/midi) is implemented in the `lv2-midi` crate, and is
re-exported in the `lv2` crate under the `midi` submodule (gated by the `midi` feature).
This way, users can choose to either depend on the `lv2` crate, or on the specific sub-crates they need.

Because we implement specifications as separate crates, we can make sure that there are no private implementation
details shared across specifications, preventing users to implement their own if needed.
This has several big advantages:

* Users can always pick and choose what they need, and not include what they don't.

Although this is unlikely to impact runtime performance, it does help to reduce the amount of dependencies,
as well as compile times and final binary sizes, which is always nice.
* Users can swap out some specification implementations for their own, while still relying on the rest of the `lv2`
crate(s).

Although the goal of this library is to cover as many use cases as possible, it may be possible for some users to
stumble upon extreme edge-cases we didn't see coming.
* With this library still being in alpha (`0.x`) state, some specifications might be incomplete, or not implemented
at all. This allows the user to put together a quick implementation that suits their needs while they wait for the
full specification to be implemented. (Pull Requests are always welcome however!)
* Users can implement (and publish) non-standard LV2 specifications on top of the `lv2` crates. This is by far the
biggest advantage, as the LV2 ecosystem also uses non-standard but useful specifications. For instance, the
[KxStudio](https://kx.studio) project has a few [extra specifications](https://kx.studio/ns/) that some plugins
implement. The [Ardour](https://ardour.org) DAW also has some non-standard specifications for their inline strip
displays, which can be quite useful.

## Lightweight, fast library

Obviously, any library handling realtime audio needs to be fast to stay within the time budgets of the given audio
buffer sizes (and avoid XRuns).

This is where Rust's "zero-cost abstractions" truly shine, as we can build higher-level abstractions that produce little
to no extra machine code to execute (and thus a minimal performance penalty). Of course, the `lv2` crate provides a
large majority of those in its APIs, a vast amount being nothing but a wrapper around the pointers given to the plugin
by the host.

However, there is another important performance consideration that is not just "how fast can we do the processing":
plugin implementations need to be as lightweight as possible. This means than both plugin authors, and the libraries
they use (such as the `lv2`), must be *extremely* conservative about the resources they allocate for themselves.
Whether it is memory, threads, or other synchronization primitives that can introduce delays or locking.

The reason for this is simple: a plugin is very rarely alone when it's used in a DAW. It is most likely to run alongside
dozens, if not hundreds, of other plugins. Not to mention the processing the DAW itself needs to do for mixing all of
these, and the I/O it needs to perform to the hardware to get actual sound. All of it in very tight timing budgets.

For instance, audio processing code may want to spread work across multiple threads to be as fast as possible. This
works fine in a standalone application where the process can allocate most or all of the CPU cores to itself. However,
this can't work in a session where there are many instances of that code competing for CPU power at the same time. The
OS would have to interrupt and reschedule threads constantly, losing most of the CPU power in context switches.

In general, the LV2 APIs consider the host to be in charge of handling most of the plugin's resources, and behave
like a scheduler or executor of sorts for the various plugins. Because it knows the state of all the plugins running,
as well as their (potentially complex) I/O configuration, it can apply massive optimizations to its scheduling and
processing, such as spreading the work on a single thread pool. However, this means the plugins must cooperate, and
cannot do what they want. Examples of work LV2 plugins should defer to the host are I/O buffers and communication, state
serialization (i.e., presets / session loading and saving), asynchronous processing, or UI communication.

In that aspect, LV2 plugins and hosts share very similar behaviors with Rust's own
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar with futures and executors (i didn't ), so I don't fully understand this section. Would it be possible to reword this section so that people can understand it without knowledge about futures and executors?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, re-reading this I realize this is a bit more of me rambling about LV2's API (compared to others like VST) more than anything else. I will reword (if not completely remove), as it's more of an example to illustrate the rest of this section.

[Futures](https://doc.rust-lang.org/std/future/trait.Future.html) and
[Executors](https://docs.rs/futures/0.3.16/futures/executor/index.html).

Like with Rust Futures, the execution system for LV2 plugins is inherently *cooperative*. LV2 plugins *must* finish
processing before another plugin can run. If a plugin, just like a Future, uses blocking operations, it will block
the whole thread without any means of interruption by the host. This includes (but is not limited to): memory
allocations, multi-thread processing, thread synchronization (atomics, locks), general I/O, and more.

If a plugin needs to perform asynchronous work for instance (like loading and decoding a sample file), they should use
the [LV2 Worker](http://lv2plug.in/ns/ext/worker) API (see the `lv2-worker` crate). Just like a Future would use the
executor's [`spawn()`](https://docs.rs/futures/0.3.16/futures/task/trait.SpawnExt.html#method.spawn) method to process
additional asynchronous work, instead of doing it synchronously or spawning a thread of its own.

## Extra ergonomics and sugar
Also, while ergonomics and extra utilities are nice to have in this library sometimes, they *must* be optional to use.
Indeed, because of the goal to be a low-level library, we must not prevent the user from doing custom things themselves
at the cost of complexity. At least, as long as it is safe for them to do so.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A safe abstraction usually introduces some ergonomics, so I wouldn't say that this must be optional.

I think designing safe abstractions will very often come with hard choices: a more complicated API, some runtime overhead (see previous section for that), something that is hard to maintain or exposing unsafe methods or traits.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, what I meant here was ergonomic*-only* APIs should be optional (like the derive macros cited in the other comment), not all APIs that happen to be more ergonomic that raw LV2's.

Pretty much all safe APIs are more ergonomic than LV2's anyway. 🙃

I agree that API design comes with hard choices. I forgot to make it explicit in the DESIGN document (which isn't completed anyway), but this is what it is about: the list at the beginning is in order of most important to least important.

For example, when it comes to the lv2 crate, Memory Safety will be chosen in favored of C Parity or Performance, all of which are considered more important than Ergonomics (which is quite low the list).

Again, will fix that. Thanks a lot for your remarks, they are quite helpful! 🙂


A good example of this is the `UridCollection` derive macro in the `urid` crate. While users can make similar
collections manually using 100% safe and sound code, it is a very tedious and boilerplate-heavy implementation that
can be abstracted away. This is thanks to the fact that `URID<T>` is integrated to the type system, allowing the
structure to be manually filled if desired.

An anti-example of this is the `PortCollection` derive macro in the `core` crate. It may seem similar to
`UridCollection` at a first glance (it is a collection of things that can be filled automatically), but there is a
catch. An invalid `index <-> port` mapping implementation means that bad pointer casts are going to be made, and this
is definitely Undefined Behavior.

In this case, this ergonomic helper is necessary to produce safe code, and is not considered optional: the
`PortCollection` abstraction cannot be bypassed.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the PortCollection abstraction can in fact be bypassed: it can be implemented outside of the lv2 crate. For rsynth, this is in fact desirable, otherwise it's hard to abstract over different API's.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is an unclear wording issue. The PortCollection trait itself cannot be bypassed: the Plugin trait (among others) rely heavily on it. However, the PortCollection derive macro can be bypassed, and you can implement PortCollection however you like outside of the lv2 crate, which, as you say, is useful if you want to make your own abstraction.

In that specific case, I consider the PortCollection trait to be necessary for safe operation, and the PortCollection derive macro to be the optional syntax sugar layer on top (like all derive macros in rust-lv2 are).

Will fix this as well. 🙂


## Host support

This library is intended to provide host support at some point. However, considering our currently limited resources,
we took the decision to first get sufficient support for implementing plugins, before focusing on host support.

However, host support will need to be (or at least, guaranteed to not have backwards-compatibility issues) before
releasing the `1.0` version of the `lv2` crate.

Host-only features will be gated behind a general `host`
[Cargo feature](https://doc.rust-lang.org/cargo/reference/features.html) and modules, as to not pollute the scope for
LV2 plugins.

Note that, when complete, this library will only provide APIs to allow hosts to instantiate and communicate with
LV2 plugins. It will not implement common LV2 host features such as plugin discovery, manifest parsing and such, like
the [Lilv](https://drobilla.net/software/lilv.html) C library does.

However, it is likely that such library will be implemented at some point, while still integrating with the `lv2` crate,
for a more complete LV2 Host development experience. It will also likely be developed under the same
[RustAudio](https://github.com/RustAudio) organization, possibly by the same authors.

## `#![no_std]` support

Technically, nothing in the LV2 APIs or specifications require any kind of Operating System support. Therefore, all LV2
APIs, including this crate, could be `#![no_std]`-compatible.

However, while running LV2 hosts and plugins is possible, it mostly seems like a curiosity at this point. Unlike DAW
support (which is already an established and common workflow), we consider `!#[no_std]` to be a nice-to-have, and we
have no intent to focus on it. However, it may come in future versions, and Pull Requests implementing it are always
welcome!