N
nk_Enuke
Guest
Introduction
This article demonstrates how to create a DuckDB extension that generates interactive charts using Rust and the Iced GUI framework. We'll build a system that executes SQL queries and displays the results as bar charts in a native window, bridging the gap between database analytics and visual representation.
Table of Contents
- Project Setup and Architecture
- Creating the Rust Library with FFI
- Building the C++ Extension Wrapper
- Implementing the Iced Chart Viewer
- Handling macOS Threading Constraints
- Dynamic Data Visualization
- Troubleshooting and Lessons Learned
Chapter 1: Project Setup and Architecture
Overview
Our architecture consists of three main components:
- A C++ DuckDB extension that provides SQL functions
- A Rust library exposed via FFI (Foreign Function Interface)
- A standalone Iced application for rendering charts
Directory Structure
Code:
iced_duck/
βββ src/ # C++ extension
β βββ quack_extension.cpp
β βββ include/
β βββ quack_extension.hpp
βββ rust_hello_duck/ # Rust library
β βββ Cargo.toml
β βββ src/
β β βββ lib.rs
β β βββ bin/
β β βββ chart_viewer.rs
βββ CMakeLists.txt
βββ Makefile
Chapter 2: Creating the Rust Library with FFI
Basic FFI Setup
We started with a simple Rust library exposing C-compatible functions:
Code:
use std::ffi::{c_char, CString};
#[no_mangle]
pub extern "C" fn rust_hello_world() -> *const c_char {
let message = CString::new("Hello from Duck!").unwrap();
message.into_raw()
}
#[no_mangle]
pub extern "C" fn rust_hello_free(s: *mut c_char) {
unsafe {
if !s.is_null() {
let _ = CString::from_raw(s);
}
}
}
Key Learnings
- Memory Management: Always provide a free function for allocated strings
- Safety Attributes: Modern Rust requires explicit unsafe markers for FFI functions
- Cross-platform Compatibility: Handle different library extensions (.dylib, .so, .dll)
Chapter 3: Building the C++ Extension Wrapper
Dynamic Library Loading
The C++ extension loads the Rust library at runtime:
Code:
void* rust_lib = dlopen(lib_path.c_str(), RTLD_LAZY);
if (!rust_lib) {
printf("Warning: Could not load Rust library\n");
return;
}
// Get function pointers
auto rust_hello_world = (rust_hello_world_fn)dlsym(rust_lib, "rust_hello_world");
Registering SQL Functions
Code:
auto rust_hello_func = ScalarFunction("rust_hello", {}, LogicalType::VARCHAR, RustHelloScalarFun);
ExtensionUtil::RegisterFunction(instance, rust_hello_func);
Chapter 4: Implementing the Iced Chart Viewer
Chart Application Structure
We created a separate binary for the chart viewer to handle GUI rendering:
Code:
struct ChartApp {
data: ChartData,
}
impl Application for ChartApp {
type Message = Message;
type Theme = Theme;
type Executor = iced::executor::Default;
type Flags = ChartData;
fn view(&self) -> Element<Message> {
let chart = canvas(self as &Self)
.width(Length::Fill)
.height(Length::Fill);
container(
column![
text(&self.data.title).size(24),
chart,
]
.spacing(10)
)
.padding(20)
.into()
}
}
Canvas Drawing
The actual chart rendering uses Iced's canvas API:
Code:
impl<Message> canvas::Program<Message> for ChartApp {
fn draw(&self, _state: &Self::State, renderer: &iced::Renderer,
_theme: &Theme, bounds: Rectangle, _cursor: iced::mouse::Cursor) -> Vec<Geometry> {
let mut frame = Frame::new(renderer, bounds.size());
// Draw bars
for (i, &value) in self.data.y_data.iter().enumerate() {
let height = (value / max_value) * chart_height * 0.8;
frame.fill_rectangle(
Point::new(x, y),
Size::new(bar_width, height as f32),
Color::from_rgb(0.2, 0.6, 0.9),
);
}
vec![frame.into_geometry()]
}
}
Chapter 5: Handling macOS Threading Constraints
The Problem
On macOS, GUI applications must run on the main thread. Our initial approach using
thread::spawn
resulted in:
Code:
thread '<unnamed>' panicked at:
on macOS, `EventLoop` must be created on the main thread!
The Solution
We launched the chart viewer as a separate process:
Code:
pub extern "C" fn rust_show_chart() -> *const c_char {
match Command::new("rust_hello_duck/target/release/chart_viewer")
.spawn() {
Ok(_) => {
let message = CString::new("Chart viewer launched").unwrap();
message.into_raw()
}
Err(e) => {
let message = CString::new(format!("Failed: {}", e)).unwrap();
message.into_raw()
}
}
}
Chapter 6: Dynamic Data Visualization
SQL Function Interface
We created a
bar_chart
function that accepts SQL queries:
Code:
SELECT bar_chart(
'SELECT category FROM sales',
'SELECT amount FROM sales',
'Sales by Category'
);
Data Transfer
Instead of complex JSON serialization, we used a simple text format:
Code:
// Save data to file
std::ofstream file("/tmp/duckdb_chart_data.txt");
file << title_str << "\n";
file << x_data << "\n"; // comma-separated
file << y_data << "\n"; // comma-separated
file.close();
Chapter 7: Troubleshooting and Lessons Learned
Common Issues Encountered
- Library Path Resolution: Always try both relative and absolute paths
- Deadlocks in Query Execution: The
context.Query()
method can cause deadlocks when called from within a DuckDB function - Type Compatibility: Careful handling of numeric types between C++ and Rust
- Build System Complexity: Managing both CMake and Cargo builds requires coordination
Best Practices
- Start Simple: Begin with basic FFI functions before adding complexity
- Debug Incrementally: Add print statements at each stage of execution
- Handle Errors Gracefully: Always provide fallback behavior
- Test Cross-platform Early: Platform-specific issues can be significant
Conclusion
This project demonstrates the power of combining different technologies:
- DuckDB for SQL processing
- Rust for safe systems programming
- Iced for modern GUI development
- FFI for language interoperability
While the integration presents challenges, particularly around threading and build systems, the result is a powerful system that can transform SQL query results into interactive visualizations.
The complete source code and additional examples are available in the project repository. Feel free to extend this foundation with additional chart types, improved error handling, or real-time data updates.
Future Enhancements
- Support for multiple chart types (line, pie, scatter)
- Real-time data streaming
- Interactive chart features (zoom, pan, tooltips)
- Better error handling and user feedback
- Cross-platform installer generation
Happy coding, and may your data always be beautifully visualized!
Continue reading...