This guide helps developers migrate from C++ JUCE to Rust JUCE-RS, covering API differences, idiom translations, and best practices.
- Overview
- Language Differences
- API Mapping
- Common Patterns
- Memory Management
- Error Handling
- Threading and Concurrency
- Audio Processing
- GUI Components
- Plugin Development
- Build System
- Testing
- Performance Considerations
JUCE-RS is a Rust reimplementation of JUCE that maintains conceptual compatibility while embracing Rust idioms. The API is designed to feel familiar to JUCE developers while leveraging Rust's safety guarantees and modern language features.
- Ownership System: Rust's ownership model replaces manual memory management
- Error Handling:
Result<T, E>instead of exceptions - Null Safety:
Option<T>instead of null pointers - Traits: Replace C++ inheritance and virtual functions
- Naming:
snake_casefor functions,PascalCasefor types - Immutability: Explicit
mutfor mutable references
C++ JUCE:
String myString;
myString.toUpperCase();
AudioBuffer<float> buffer;
buffer.applyGain(0.5f);Rust JUCE-RS:
let my_string = JString::new("hello");
my_string.to_uppercase();
let mut buffer = AudioBuffer::<f32>::new(2, 512);
buffer.apply_gain(0.5);C++ JUCE:
Component* component = new Component();
Component& ref = *component;
delete component;Rust JUCE-RS:
// Owned value
let component = Component::new();
// Borrowed reference
let ref_component = &component;
// Mutable reference
let mut component = Component::new();
let mut_ref = &mut component;
// No manual deletion - automatic cleanupC++ JUCE:
String* str = nullptr;
if (str != nullptr) {
str->toUpperCase();
}Rust JUCE-RS:
let str: Option<JString> = None;
if let Some(s) = str {
s.to_uppercase();
}
// Or using match
match str {
Some(s) => s.to_uppercase(),
None => JString::new(""),
}| C++ JUCE | Rust JUCE-RS | Notes |
|---|---|---|
String |
JString |
UTF-8 by default in Rust |
StringArray |
StringArray |
Similar API |
Array<T> |
Array<T> or Vec<T> |
Consider using Vec<T> for simple cases |
OwnedArray<T> |
Vec<Box<T>> |
Rust's Box provides heap allocation |
ReferenceCountedArray<T> |
Vec<Arc<T>> |
Arc for shared ownership |
HashMap<K,V> |
HashMap<K,V> |
Rust's std HashMap or JUCE-RS wrapper |
File |
File |
Similar API |
MemoryBlock |
Vec<u8> |
Rust's Vec is more idiomatic |
var |
serde_json::Value |
For dynamic JSON values |
| C++ JUCE | Rust JUCE-RS | Notes |
|---|---|---|
AudioBuffer<float> |
AudioBuffer<f32> |
Generic over sample type |
MidiMessage |
MidiMessage |
Similar API |
MidiBuffer |
MidiBuffer |
Similar API |
AudioPlayHead |
AudioPlayHead |
Trait instead of abstract class |
Synthesiser |
Synthesiser |
Similar structure |
SynthesiserVoice |
SynthesiserVoice |
Trait instead of abstract class |
| C++ JUCE | Rust JUCE-RS | Notes |
|---|---|---|
Component |
Component |
Trait-based instead of inheritance |
Button |
Button |
Similar API |
Slider |
Slider |
Similar API |
Label |
Label |
Similar API |
Graphics |
Graphics |
Similar drawing API |
Colour |
Colour |
Similar color representation |
Image |
Image |
Similar image handling |
| C++ JUCE | Rust JUCE-RS | Notes |
|---|---|---|
dsp::IIR::Filter |
IIRFilter |
Similar API |
dsp::Oscillator |
Oscillator |
Similar API |
dsp::Convolution |
Convolution |
Similar API |
dsp::Gain |
Gain |
Generic over sample type |
dsp::ProcessSpec |
ProcessSpec |
Similar structure |
C++ JUCE:
class MyComponent : public Component {
public:
void paint(Graphics& g) override {
g.fillAll(Colours::black);
}
void resized() override {
button.setBounds(10, 10, 100, 30);
}
private:
TextButton button;
};Rust JUCE-RS:
use juce_gui_basics::{Component, ComponentTrait, Button};
use juce_graphics::{Graphics, Colour};
struct MyComponent {
button: Button,
}
impl ComponentTrait for MyComponent {
fn paint(&mut self, g: &mut Graphics) {
g.fill_all(Colour::black());
}
fn resized(&mut self) {
self.button.set_bounds(10, 10, 100, 30);
}
}C++ JUCE:
class MyListener : public Button::Listener {
public:
void buttonClicked(Button* button) override {
// Handle click
}
};
button.addListener(&myListener);Rust JUCE-RS:
// Using closures
button.on_click(|| {
// Handle click
});
// Or using traits
trait ButtonListener {
fn button_clicked(&mut self, button: &Button);
}
struct MyListener;
impl ButtonListener for MyListener {
fn button_clicked(&mut self, button: &Button) {
// Handle click
}
}
button.add_listener(Box::new(MyListener));C++ JUCE:
std::unique_ptr<Component> component = std::make_unique<Component>();
std::shared_ptr<AudioProcessor> processor = std::make_shared<AudioProcessor>();Rust JUCE-RS:
// Box for unique ownership (heap allocation)
let component = Box::new(Component::new());
// Arc for shared ownership (thread-safe reference counting)
let processor = Arc::new(AudioProcessor::new());
// Rc for shared ownership (single-threaded)
let processor = Rc::new(AudioProcessor::new());C++ JUCE:
void processAudio(std::function<void(float)> callback) {
callback(0.5f);
}
processAudio([](float value) {
// Handle value
});Rust JUCE-RS:
fn process_audio<F>(callback: F)
where
F: FnOnce(f32),
{
callback(0.5);
}
process_audio(|value| {
// Handle value
});C++ JUCE:
void processFile() {
File file("data.txt");
auto stream = file.createInputStream();
// stream automatically deleted when out of scope
}Rust JUCE-RS:
fn process_file() -> Result<()> {
let file = File::new("data.txt");
let stream = file.create_input_stream()?;
// stream automatically dropped when out of scope
Ok(())
}C++ JUCE:
class MyObject : public ReferenceCountedObject {
// ...
};
ReferenceCountedObjectPtr<MyObject> ptr = new MyObject();Rust JUCE-RS:
use std::sync::Arc;
struct MyObject {
// ...
}
let ptr = Arc::new(MyObject { /* ... */ });
let ptr2 = Arc::clone(&ptr); // Increment reference countC++ JUCE:
WeakReference<Component> weakRef = component;
if (auto* comp = weakRef.get())
comp->repaint();Rust JUCE-RS:
use std::sync::{Arc, Weak};
let component = Arc::new(Component::new());
let weak_ref: Weak<Component> = Arc::downgrade(&component);
if let Some(comp) = weak_ref.upgrade() {
comp.repaint();
}C++ JUCE:
try {
File file("data.txt");
auto content = file.loadFileAsString();
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
}Rust JUCE-RS:
use juce_core::{File, Result};
fn load_file() -> Result<String> {
let file = File::new("data.txt");
file.read_to_string()
}
match load_file() {
Ok(content) => println!("Content: {}", content),
Err(e) => eprintln!("Error: {}", e),
}
// Or using ? operator
fn process_file() -> Result<()> {
let file = File::new("data.txt");
let content = file.read_to_string()?;
// Process content
Ok(())
}C++ JUCE:
jassert(value > 0);
jassertfalse; // Should never reach hereRust JUCE-RS:
assert!(value > 0);
assert!(value > 0, "Value must be positive: {}", value);
unreachable!(); // Should never reach here
panic!("Unexpected condition");C++ JUCE:
class MyThread : public Thread {
public:
void run() override {
while (!threadShouldExit()) {
// Do work
}
}
};
MyThread thread;
thread.startThread();
thread.stopThread(1000);Rust JUCE-RS:
use std::thread;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
let should_exit = Arc::new(AtomicBool::new(false));
let should_exit_clone = Arc::clone(&should_exit);
let handle = thread::spawn(move || {
while !should_exit_clone.load(Ordering::Relaxed) {
// Do work
}
});
// Signal thread to exit
should_exit.store(true, Ordering::Relaxed);
handle.join().unwrap();C++ JUCE:
CriticalSection mutex;
void accessSharedData() {
const ScopedLock lock(mutex);
// Access shared data
}Rust JUCE-RS:
use std::sync::Mutex;
let mutex = Mutex::new(0);
fn access_shared_data(mutex: &Mutex<i32>) {
let mut data = mutex.lock().unwrap();
*data += 1;
// Lock automatically released when `data` goes out of scope
}C++ JUCE:
std::atomic<float> gain{0.5f};
// From UI thread
gain.store(0.7f, std::memory_order_relaxed);
// From audio thread
float currentGain = gain.load(std::memory_order_relaxed);Rust JUCE-RS:
use std::sync::atomic::{AtomicU32, Ordering};
let gain = AtomicU32::new(0.5f32.to_bits());
// From UI thread
gain.store(0.7f32.to_bits(), Ordering::Relaxed);
// From audio thread
let current_gain = f32::from_bits(gain.load(Ordering::Relaxed));C++ JUCE:
class MyMessage : public Message {
public:
void messageCallback() override {
// Handle message
}
};
MessageManager::getInstance()->postMessage(new MyMessage());Rust JUCE-RS:
use juce_events::{Message, MessageManager};
struct MyMessage;
impl Message for MyMessage {
fn message_callback(&mut self) {
// Handle message
}
}
MessageManager::post_message(Box::new(MyMessage));C++ JUCE:
class MyProcessor : public AudioProcessor {
public:
void prepareToPlay(double sampleRate, int samplesPerBlock) override {
// Prepare
}
void processBlock(AudioBuffer<float>& buffer, MidiBuffer& midi) override {
// Process
buffer.applyGain(0.5f);
}
void releaseResources() override {
// Clean up
}
};Rust JUCE-RS:
use juce_audio_processors::AudioProcessor;
use juce_audio_basics::{AudioBuffer, MidiBuffer};
struct MyProcessor {
sample_rate: f64,
}
impl AudioProcessor for MyProcessor {
fn prepare_to_play(&mut self, sample_rate: f64, max_block_size: usize) {
self.sample_rate = sample_rate;
}
fn process_block(&mut self, buffer: &mut AudioBuffer<f32>, midi: &MidiBuffer) {
buffer.apply_gain(0.5);
}
fn release_resources(&mut self) {
// Clean up
}
}C++ JUCE:
void processBuffer(AudioBuffer<float>& buffer) {
for (int channel = 0; channel < buffer.getNumChannels(); ++channel) {
float* channelData = buffer.getWritePointer(channel);
for (int sample = 0; sample < buffer.getNumSamples(); ++sample) {
channelData[sample] *= 0.5f;
}
}
}Rust JUCE-RS:
fn process_buffer(buffer: &mut AudioBuffer<f32>) {
for channel in 0..buffer.get_num_channels() {
if let Some(channel_data) = buffer.get_write_pointer(channel) {
for sample in channel_data.iter_mut() {
*sample *= 0.5;
}
}
}
}C++ JUCE:
void processMidi(MidiBuffer& midi) {
for (const auto metadata : midi) {
auto message = metadata.getMessage();
if (message.isNoteOn()) {
int note = message.getNoteNumber();
int velocity = message.getVelocity();
// Handle note on
}
}
}Rust JUCE-RS:
fn process_midi(midi: &MidiBuffer) {
for (message, _timestamp) in midi.iter() {
if message.is_note_on() {
let note = message.get_note_number();
let velocity = message.get_velocity();
// Handle note on
}
}
}C++ JUCE:
class MainComponent : public Component {
public:
MainComponent() {
addAndMakeVisible(button);
addAndMakeVisible(slider);
}
void resized() override {
auto bounds = getLocalBounds();
button.setBounds(bounds.removeFromTop(30));
slider.setBounds(bounds.removeFromTop(30));
}
private:
TextButton button;
Slider slider;
};Rust JUCE-RS:
use juce_gui_basics::{Component, ComponentTrait, Button, Slider};
struct MainComponent {
button: Button,
slider: Slider,
}
impl MainComponent {
fn new() -> Self {
let mut component = Self {
button: Button::new(),
slider: Slider::new(),
};
component.add_and_make_visible(&component.button);
component.add_and_make_visible(&component.slider);
component
}
}
impl ComponentTrait for MainComponent {
fn resized(&mut self) {
let bounds = self.get_local_bounds();
let (top, rest) = bounds.remove_from_top(30);
self.button.set_bounds(top);
let (top, _rest) = rest.remove_from_top(30);
self.slider.set_bounds(top);
}
}C++ JUCE:
void paint(Graphics& g) override {
g.fillAll(Colours::black);
g.setColour(Colours::white);
g.drawRect(getLocalBounds(), 2);
g.drawText("Hello", getLocalBounds(), Justification::centred);
}Rust JUCE-RS:
fn paint(&mut self, g: &mut Graphics) {
g.fill_all(Colour::black());
g.set_colour(Colour::white());
g.draw_rect(self.get_local_bounds(), 2.0);
g.draw_text("Hello", self.get_local_bounds(), Justification::Centred);
}C++ JUCE:
class MyPlugin : public AudioProcessor {
public:
MyPlugin() {
addParameter(gain = new AudioParameterFloat(
"gain", "Gain", 0.0f, 1.0f, 0.5f));
}
private:
AudioParameterFloat* gain;
};Rust JUCE-RS:
use juce_audio_processors::{AudioProcessor, AudioParameterFloat};
use std::sync::Arc;
struct MyPlugin {
gain: Arc<AudioParameterFloat>,
}
impl MyPlugin {
fn new() -> Self {
Self {
gain: Arc::new(AudioParameterFloat::new(
"gain",
"Gain",
0.0,
1.0,
0.5,
)),
}
}
}C++ JUCE:
void getStateInformation(MemoryBlock& destData) override {
auto state = parameters.copyState();
std::unique_ptr<XmlElement> xml(state.createXml());
copyXmlToBinary(*xml, destData);
}
void setStateInformation(const void* data, int sizeInBytes) override {
std::unique_ptr<XmlElement> xml(getXmlFromBinary(data, sizeInBytes));
if (xml != nullptr)
parameters.replaceState(ValueTree::fromXml(*xml));
}Rust JUCE-RS:
fn get_state_information(&self) -> Result<Vec<u8>> {
let state = self.parameters.copy_state();
let xml = state.to_xml()?;
Ok(xml.to_string().into_bytes())
}
fn set_state_information(&mut self, data: &[u8]) -> Result<()> {
let xml_str = String::from_utf8(data.to_vec())?;
let xml = XmlElement::from_string(&xml_str)?;
let state = ValueTree::from_xml(&xml)?;
self.parameters.replace_state(state);
Ok(())
}C++ JUCE (CMakeLists.txt):
juce_add_plugin(MyPlugin
PLUGIN_MANUFACTURER_CODE Manu
PLUGIN_CODE Plug
FORMATS VST3 AU
PRODUCT_NAME "My Plugin")
target_sources(MyPlugin PRIVATE
Source/PluginProcessor.cpp
Source/PluginEditor.cpp)
target_link_libraries(MyPlugin PRIVATE
juce::juce_audio_basics
juce::juce_audio_processors)Rust JUCE-RS (Cargo.toml):
[package]
name = "my-plugin"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
juce-audio-basics = "0.1"
juce-audio-processors = "0.1"
juce-audio-plugin-client = "0.1"
[package.metadata.juce]
manufacturer_code = "Manu"
plugin_code = "Plug"
formats = ["VST3", "AU"]
product_name = "My Plugin"C++ JUCE:
class MyTests : public UnitTest {
public:
MyTests() : UnitTest("My Tests") {}
void runTest() override {
beginTest("String operations");
String s = "hello";
expect(s.toUpperCase() == "HELLO");
}
};
static MyTests myTests;Rust JUCE-RS:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_string_operations() {
let s = JString::new("hello");
assert_eq!(s.to_uppercase().as_str(), "HELLO");
}
}Rust JUCE-RS:
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn test_buffer_operations(gain in 0.0f32..1.0f32) {
let mut buffer = AudioBuffer::<f32>::new(2, 512);
buffer.apply_gain(gain);
// Verify properties
}
}
}C++ JUCE:
void processBlock(AudioBuffer<float>& buffer, MidiBuffer& midi) override {
// Avoid: String message = "Processing"; // Allocation!
// Good: Pre-allocated buffers
tempBuffer.setSize(buffer.getNumChannels(), buffer.getNumSamples(), false, false, false);
}Rust JUCE-RS:
fn process_block(&mut self, buffer: &mut AudioBuffer<f32>, midi: &MidiBuffer) {
// Avoid: let message = String::from("Processing"); // Allocation!
// Good: Pre-allocated buffers
self.temp_buffer.set_size(
buffer.get_num_channels(),
buffer.get_num_samples(),
);
}C++ JUCE:
FloatVectorOperations::add(dest, src, numSamples);Rust JUCE-RS:
use juce_audio_basics::FloatVectorOperations;
FloatVectorOperations::add(dest, src, num_samples);Rust's zero-cost abstractions mean that high-level code compiles to the same machine code as low-level code:
// High-level iterator code
buffer.get_write_pointer(0)
.unwrap()
.iter_mut()
.for_each(|sample| *sample *= 0.5);
// Compiles to the same assembly as:
let samples = buffer.get_write_pointer(0).unwrap();
for i in 0..samples.len() {
samples[i] *= 0.5;
}// Wrong
let buffer = AudioBuffer::<f32>::new(2, 512);
buffer.apply_gain(0.5); // Error: cannot borrow as mutable
// Correct
let mut buffer = AudioBuffer::<f32>::new(2, 512);
buffer.apply_gain(0.5);let buffer = AudioBuffer::<f32>::new(2, 512);
process_buffer(buffer); // buffer moved here
// buffer.clear(); // Error: value used after move
// Solution: Use references
process_buffer(&mut buffer);
buffer.clear(); // OK// Wrong
button.on_click(|| {
self.value = 10; // Error: cannot capture `self`
});
// Correct: Use Arc/Mutex for shared state
let value = Arc::clone(&self.value);
button.on_click(move || {
*value.lock().unwrap() = 10;
});// Wrong
let content = file.read_to_string(); // Error: Result not handled
// Correct
let content = file.read_to_string()?;
// Or
let content = file.read_to_string().unwrap_or_default();- Replace
new/deletewith Rust ownership - Convert exceptions to
Result<T, E> - Replace null pointers with
Option<T> - Convert inheritance to traits
- Update naming conventions (snake_case)
- Replace raw pointers with references
- Convert callbacks to closures or trait objects
- Update build system (CMake → Cargo)
- Rewrite tests using Rust test framework
- Add property-based tests where appropriate
- Profile and optimize performance
- Test on all target platforms
If you encounter issues during migration:
- Check the API documentation
- Review the examples
- Search GitHub issues
- Ask on Rust Audio Discord
- Open a new issue with a minimal reproducible example
Found an error in this guide or have suggestions? Please open an issue or pull request!