Mobile Development 15 min read

Exploring Rust-Based UI Rendering on Android: Architecture, Hardware Acceleration, and JNI Integration

This article explores the feasibility of using Rust for UI rendering on Android by analyzing the rust-windowing ecosystem, detailing both software and hardware rendering approaches, explaining the integration of winit and glutin for cross-platform window and OpenGL context management, and demonstrating JNI-based communication with Android Surface components.

ByteDance Dali Intelligent Technology Team
ByteDance Dali Intelligent Technology Team
ByteDance Dali Intelligent Technology Team
Exploring Rust-Based UI Rendering on Android: Architecture, Hardware Acceleration, and JNI Integration

Background: Rust's safety, performance, and FFI capabilities make it ideal for cross-platform libraries. This article investigates its potential for UI rendering on Android by analyzing the rust-windowing ecosystem and integrating it with the Android NDK.

Software Rendering: Using android-ndk-rs, Rust wraps ANativeWindow to perform software drawing via FFI, mimicking Java's Canvas API. While functional for basic shapes, this CPU-bound approach lacks the performance required for complex UIs.

unsafe fn draw_rect_on_window(nativewindow: &NativeWindow, colors: Vec
, rect: ndk_glue::Rect) {
    let height = nativewindow.height();
    let width = nativewindow.width();
    let color_format = get_color_format();
    let format = color_format.0;
    let bbp = color_format.1;
    nativewindow.set_buffers_geometry(width, height, format);
    nativewindow.acquire();
    let mut buffer = NativeWindow::generate_epmty_buffer(width, height, width, format);
    let locked = nativewindow.lock(&mut buffer, &mut NativeWindow::generate_empty_rect(0, 0, width, height));
    if locked < 0 {
        nativewindow.release();
        return;
    }
    draw_rect_into_buffer(buffer.bits, colors, rect, width, height);
    let result = nativewindow.unlock_and_post();
    nativewindow.release();
}
unsafe fn draw_rect_into_buffer(bits: *mut ::std::os::raw::c_void, colors: Vec
, rect: ndk_glue::Rect, window_width: i32, window_height: i32) {
    let bbp = colors.len() as u32;
    let window_width = window_width as u32;
    for i in rect.top+1..=rect.bottom {
        for j in rect.left+1..=rect.right {
            let cur = (j + (i-1) * window_width - 1) * bbp;
            for k in 0..bbp {
                *(bits.offset((cur + (k as u32)) as isize) as *mut u8) = colors[k as usize];
            }
        }
    }
}

Hardware Rendering & Window System (winit): The winit crate provides a cross-platform Window abstraction and an EventLoop driven by Android's ALooper. It manages window properties, handles input via InputQueue, and uses file descriptor pipes to wake the event loop for custom and system events.

// src/window.rs
pub struct Window {
    pub(crate) window: platform_impl::Window,
}
impl Window {
    #[inline]
    pub fn request_redraw(&self) {
        self.window.request_redraw()
    }
    pub fn inner_position(&self) -> Result
, NotSupportedError> {
        self.window.inner_position()
    }
    pub fn current_monitor(&self) -> Option
{
        self.window.current_monitor()
    }
}
pub struct WindowId(pub(crate) platform_impl::WindowId);
pub struct WindowBuilder {
    pub window: WindowAttributes,
    pub(crate) platform_specific: platform_impl::PlatformSpecificWindowBuilderAttributes,
}
impl WindowBuilder {
    #[inline]
    pub fn build
(
        self,
        window_target: &EventLoopWindowTarget
,
    ) -> Result
{
        platform_impl::Window::new(&window_target.p, self.window, self.platform_specific).map(
            |window| {
                window.request_redraw();
                Window { window }
            },
        )
    }
}

OpenGL Context (glutin): glutin bridges winit and OpenGL(ES) by managing EGL contexts. It associates rendering contexts with windows through WindowedContext, initializing EGL surfaces for drawing and reading operations.

Hardware Rendering Example: By combining winit and glutin, developers can create an EGL context, compile shaders, bind vertex buffers, and issue OpenGL draw calls within the event loop to render dynamic graphics.

fn render(&mut self, gl: &Gl) {
    let time_elapsed = self.startTime.elapsed().as_millis();
    let percent = (time_elapsed % 5000) as f32 / 5000f32;
    let angle = percent * 2f32 * std::f32::consts::PI;
    unsafe {
        let vs = gl.CreateShader(gl::VERTEX_SHADER);
        gl.ShaderSource(vs, 1, [VS_SRC.as_ptr() as *const _].as_ptr(), std::ptr::null());
        gl.CompileShader(vs);
        let fs = gl.CreateShader(gl::FRAGMENT_SHADER);
        gl.ShaderSource(fs, 1, [FS_SRC.as_ptr() as *const _].as_ptr(), std::ptr::null());
        gl.CompileShader(fs);
        let program = gl.CreateProgram();
        gl.AttachShader(program, vs);
        gl.AttachShader(program, fs);
        gl.LinkProgram(program);
        gl.UseProgram(program);
        gl.DeleteShader(vs);
        gl.DeleteShader(fs);
        let mut vb = std::mem::zeroed();
        gl.GenBuffers(1, &mut vb);
        gl.BindBuffer(gl::ARRAY_BUFFER, vb);
        let vertex = [
            SIDE_LEN * (BASE_V_LEFT+angle).cos(), SIDE_LEN * (BASE_V_LEFT+angle).sin(),  0.0,  0.4,  0.0,
            SIDE_LEN * (BASE_V_TOP+angle).cos(),  SIDE_LEN * (BASE_V_TOP+angle).sin(),  0.0,  0.4,  0.0,
            SIDE_LEN * (BASE_V_RIGHT+angle).cos(), SIDE_LEN * (BASE_V_RIGHT+angle).sin(),  0.0,  0.4,  0.0,
        ];
        gl.BufferData(
            gl::ARRAY_BUFFER,
            (vertex.len() * std::mem::size_of::
()) as gl::types::GLsizeiptr,
            vertex.as_ptr() as *const _,
            gl::STATIC_DRAW,
        );
        if gl.BindVertexArray.is_loaded() {
            let mut vao = std::mem::zeroed();
            gl.GenVertexArrays(1, &mut vao);
            gl.BindVertexArray(vao);
        }
        let pos_attrib = gl.GetAttribLocation(program, b"position\0".as_ptr() as *const _);
        let color_attrib = gl.GetAttribLocation(program, b"color\0".as_ptr() as *const _);
        gl.VertexAttribPointer(pos_attrib as gl::types::GLuint, 2, gl::FLOAT, 0, 5 * std::mem::size_of::
() as gl::types::GLsizei, std::ptr::null());
        gl.VertexAttribPointer(color_attrib as gl::types::GLuint, 3, gl::FLOAT, 0, 5 * std::mem::size_of::
() as gl::types::GLsizei, (2 * std::mem::size_of::
()) as *const () as *const _);
        gl.EnableVertexAttribArray(pos_attrib as gl::types::GLuint);
        gl.EnableVertexAttribArray(color_attrib as gl::types::GLuint);
        gl.ClearColor(1.3 * (percent-0.5).abs(), 0., 1.3 * (0.5 - percent).abs(), 1.0);
        gl.Clear(gl::COLOR_BUFFER_BIT);
        gl.DrawArrays(gl::TRIANGLES, 0, 3);
    }
}

Android-JNI Integration: Since Rust rendering relies on ANativeWindow, it can be integrated with Android's Surface via JNI. Developers must ensure the Rust rendering thread has an initialized ALooper to properly handle the EventLoop.

// Android
surface_view.holder.addCallback(object : SurfaceHolder.Callback2 {
    override fun surfaceCreated(p0: SurfaceHolder) {
        RustUtils.drawColorTriangle(surface, Color.RED)
    }
    override fun surfaceChanged(p0: SurfaceHolder, p1: Int, p2: Int, p3: Int) {}
    override fun surfaceDestroyed(p0: SurfaceHolder) {}
    override fun surfaceRedrawNeeded(p0: SurfaceHolder) {}
})
// Rust
pub unsafe extern fn Java_com_example_rust_1demo_RustUtils_drawColorTriangle__Landroid_view_Surface_2I(env: *mut JNIEnv, _: JClass, surface: jobject, color: jint) -> jboolean {
    println!("call Java_com_example_rust_1demo_RustUtils_drawColor__Landroid_view_Surface_2I");
    ndk_glue::set_native_window(NativeWindow::from_surface(env, surface));
    runner::start();
    0
}

Conclusion: The feasibility of Rust-based UI rendering on Android is proven. Future work will focus on performance benchmarking and identifying suitable production use cases.

Cross‑PlatformUI renderingRustAndroid DevelopmentJNIglutinOpenGL ESwinit
ByteDance Dali Intelligent Technology Team
Written by

ByteDance Dali Intelligent Technology Team

Technical practice sharing from the ByteDance Dali Intelligent Technology Team

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.