Overview
Dala uses Rustler to create NIFs (Native Implemented Functions) that bridge Elixir with native code written in Rust. This guide explains how to extend Dala with your own Rust code, handle platform-specific dispatch, and implement message sending.
Architecture note: Dala's NIF does not cache
Envacross calls (unsafe).cache_env/1is a stub kept for API compatibility. Message sending from ObjC/Java callbacks uses platform-specific dispatch, not cached env.
When to Use Rustler in Dala
Use Rustler when you need to:
- Call platform-specific native APIs (iOS/Android) from Elixir
- Perform CPU-intensive operations that benefit from Rust's performance
- Integrate existing Rust libraries into your Dala app
- Create custom native components not covered by Dala's built-in NIF
Project Structure
Dala's Rust code lives in:
dala/native/dala_nif/- Main NIF library (Rustler-based)dala/native/dala_nif/src/ios.rs- iOS-specific (ObjC messaging)dala/native/dala_nif/src/android.rs- Android-specific (JNI)dala/native/dala_nif/src/common.rs- Shared code and platform dispatch
Creating a New NIF Function
1. Add the Rust function
Edit dala/native/dala_nif/src/lib.rs:
use rustler::{Env, NifResult, Term};
#[rustler::nif]
fn my_custom_function<'a>(env: Env<'a>, input: Term<'a>) -> NifResult<Term<'a>> {
// Your Rust implementation here
let input_str: String = input.decode()?;
let result = format!("Processed: {}", input_str);
// Return the result
Ok(rustler::types::binary::Binary::from_bytes(result.as_bytes())
.to_term(env)
.unwrap())
}
// Register the function in the rustler::init! macro at the bottom of the file:
rustler::init!(
"Elixir.Dala.Platform.Native",
[
// ... existing functions ...
my_custom_function,
]
);2. Add the Elixir wrapper
Edit dala/lib/dala/platform/native.ex:
defmodule Dala.Platform.Native do
# ... existing functions ...
@doc "My custom function"
def my_custom_function(_input), do: :erlang.nif_error(:nif_not_loaded)
end3. Build the Rust library
iOS:
cd dala/ios/rust
cargo build --release --target aarch64-apple-ios
cargo build --release --target x86_64-apple-ios # for simulator
Android:
cd dala/android/jni/rust
cargo build --release --target aarch64-linux-android
cargo build --release --target armv7-linux-androideabi
Platform-Specific Code
Use conditional compilation to handle platform differences:
#[cfg(target_os = "ios")]
mod ios {
pub fn platform_specific() -> &'static str {
"Running on iOS"
}
}
#[cfg(target_os = "android")]
mod android {
pub fn platform_specific() -> &'static str {
"Running on Android"
}
}
pub fn get_platform_info() -> String {
#[cfg(target_os = "ios")]
return ios::platform_specific().to_string();
#[cfg(target_os = "android")]
return android::platform_specific().to_string();
#[cfg(not(any(target_os = "ios", target_os = "android")))]
return "Unknown platform".to_string();
}Calling Objective-C/Swift from Rust (iOS)
Dala uses FFI (Foreign Function Interface) to call into iOS frameworks:
use objc::runtime::{Class, Object};
use objc::{class, msg_send};
pub fn ios_specific_task() {
unsafe {
let cls: *mut Object = msg_send![class!(SomeiOSClass), sharedInstance];
if !cls.is_null() {
let _: () = msg_send![cls, someMethod];
}
}
}Calling Java from Rust (Android)
Use JNI (Java Native Interface) to call Android APIs:
use jni::objects::{JClass, JString};
use jni::JNIEnv;
pub fn android_specific_task(env: &mut JNIEnv) {
let class = env.find_class("com/example/dala/dalaBridge").unwrap();
let method = env.get_static_method_id(class, "someMethod", "()V").unwrap();
// Call the method...
}Message Delivery from Native to Elixir
The Challenge
Functions like dala_deliver_webview_eval_result are:
- Called from ObjC (iOS) or Java (Android) callbacks
- Need to send messages to Erlang processes (e.g.,
:dala_screen) - Don't have direct access to
Envfrom the NIF context
Current Approach: Platform-Specific Dispatch
Dala uses platform-specific dispatch rather than cached Env:
- iOS: ObjC message passing via
msg_send! - Android: JNI calls via
jnicrate - No caching:
cache_env/1is a no-op stub for API compatibility
Current Status (as of dala 0.0.x):
- ✅
cache_envstub exists for API compatibility - ✅
dala_deliver_webview_eval_resultlogs results viaeprintln! - ❌ Direct message sending from callbacks not yet implemented
For now, use eprintln! for debugging from native callbacks.
The Proper Way: Use Erlang C API
The correct way to send messages from C code (which Rust can call) is to use Erlang's C API:
#include <erl_nif.h>
// Get process ID by name
ErlNifPid* pid = enif_whereis(env, "dala_screen");
// Send a tuple message
ErlNifTerm* message = enif_make_tuple(env, 3,
enif_make_atom(env, "webview"),
enif_make_atom(env, "eval_result"),
enif_make_binary(env, json, strlen(json))
);
enif_send(env, pid, message, 0);Recommended Implementation Steps
- For now (stub): Log the result using
eprintln! - Short term: Implement using Erlang C API FFI
- Long term: Create proper Rust bindings for Erlang C API
Environment Handling
Dala's NIF does not cache Env across NIF calls (unsafe, lifetime issues). cache_env/1 is a no-op stub kept for API compatibility:
// lib.rs
#[rustler::nif]
fn cache_env<'a>(env: Env<'a>) -> NifResult<Term<'a>> {
// Env cannot be safely cached - lifetime tied to NIF call
// This stub exists for API compatibility
ok(env)
}Message sending from ObjC/Java callbacks uses platform-specific dispatch, not a cached environment.
Testing Your NIF
1. Unit tests in Rust:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_my_function() {
// Test your Rust code
}
}2. Integration tests from Elixir:
test "my custom function works" do
assert {:ok, result} = Dala.Platform.Native.my_custom_function("test")
endDebugging Tips
Use logging:
- Rust:
eprintln!("Debug: {}", value);(shows in logcat on Android, stderr on iOS) - iOS: Use
NSLogvia FFI - Elixir: Use
:dala_nif.log/1for early startup,LoggerafterDala.App.start
- Rust:
Check NIF loading:
:code.is_loaded(Dala.Platform.Native)Test NIF functions directly:
:dala_nif.my_custom_function("test")
Best Practices
- Error handling: Always return proper NIF errors
- Memory safety: Use Rust's ownership system, avoid unsafe when possible
- Platform checks: Use
#[cfg()]attributes for platform-specific code - Documentation: Document your NIF functions in both Rust and Elixir
- Testing: Write tests for both Rust and Elixir sides
Examples from Dala's Codebase
dala/native/dala_nif/src/lib.rs- Main NIF entry pointdala/native/dala_nif/src/ios.rs- iOS-specific implementationsdala/native/dala_nif/src/android.rs- Android-specific implementationsdala/native/dala_nif/src/common.rs- Shared code and platform dispatch