Skip to content

Latest commit

 

History

History
1017 lines (811 loc) · 21.4 KB

File metadata and controls

1017 lines (811 loc) · 21.4 KB

Migration Guide: C++ JUCE to Rust JUCE-RS

This guide helps developers migrate from C++ JUCE to Rust JUCE-RS, covering API differences, idiom translations, and best practices.

Table of Contents

  1. Overview
  2. Language Differences
  3. API Mapping
  4. Common Patterns
  5. Memory Management
  6. Error Handling
  7. Threading and Concurrency
  8. Audio Processing
  9. GUI Components
  10. Plugin Development
  11. Build System
  12. Testing
  13. Performance Considerations

Overview

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.

Key Differences

  • 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_case for functions, PascalCase for types
  • Immutability: Explicit mut for mutable references

Language Differences

Naming Conventions

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);

Pointers and References

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 cleanup

Null Safety

C++ 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(""),
}

API Mapping

Core Types

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

Audio Types

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

GUI Types

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

DSP Types

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

Common Patterns

Inheritance → Traits

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);
    }
}

Listeners → Closures/Traits

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));

Smart Pointers

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());

Callbacks

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
});

Memory Management

RAII and Ownership

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(())
}

Reference Counting

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 count

Weak References

C++ 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();
}

Error Handling

Exceptions → Result

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(())
}

Assertions

C++ JUCE:

jassert(value > 0);
jassertfalse; // Should never reach here

Rust JUCE-RS:

assert!(value > 0);
assert!(value > 0, "Value must be positive: {}", value);
unreachable!(); // Should never reach here
panic!("Unexpected condition");

Threading and Concurrency

Thread Creation

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();

Mutex and Locks

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
}

Atomic Operations

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));

Message Queue

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));

Audio Processing

Audio Processor

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
    }
}

Buffer Processing

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;
            }
        }
    }
}

MIDI Processing

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
        }
    }
}

GUI Components

Component Hierarchy

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);
    }
}

Custom Drawing

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);
}

Plugin Development

Plugin Parameters

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,
            )),
        }
    }
}

State Management

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(())
}

Build System

CMake → Cargo

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"

Testing

Unit Tests

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");
    }
}

Property-Based Tests

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
        }
    }
}

Performance Considerations

Avoiding Allocations in Audio Code

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(),
    );
}

SIMD Operations

C++ JUCE:

FloatVectorOperations::add(dest, src, numSamples);

Rust JUCE-RS:

use juce_audio_basics::FloatVectorOperations;

FloatVectorOperations::add(dest, src, num_samples);

Zero-Cost Abstractions

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;
}

Common Pitfalls

1. Forgetting mut for Mutable References

// 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);

2. Moving Values Unintentionally

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

3. Lifetime Issues with Callbacks

// 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;
});

4. Not Handling Errors

// 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();

Migration Checklist

  • Replace new/delete with 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

Resources

Getting Help

If you encounter issues during migration:

  1. Check the API documentation
  2. Review the examples
  3. Search GitHub issues
  4. Ask on Rust Audio Discord
  5. Open a new issue with a minimal reproducible example

Contributing

Found an error in this guide or have suggestions? Please open an issue or pull request!