Getting Started with Rust in AOSP
2024-03-27
Using SQLite to read the Android SMS database in Rust
When I started playing with the AOSP source code, I read a bunch of fragmented documentation most of which I tried unsuccessfully stringing together with ChatGPT. In fact, lots of ChatGPT’s recommendations led me astray, so I wanted to share a more tutorial-style guide on doing practical things in an AOSP module.
Hello Android
The quickest way to get started with writing an AOSP module in Rust is to create a new directory at the top level of your AOSP source code. Let’s call it hello-android
. This new directory is where your module will live. All AOSP modules require an Android.bp
file to tell the build system how to build your module.
Inside of your Android.bp
file, you need to define the Rust binary that we are going to build and run. Blueprint files have a json-style structure where you define the different dependencies and sources that you’re building from. In the first example, we’re just going to print out “Hello Android”, so we will have a very simple Android.bp
definition:
rust_binary {
name: "hello_android",
crate_name: "hello_android",
srcs: ["src/main.rs"],
}
Once we have this, we need the main.rs we reference above. Create a src
directory, and add a main.rs
file that looks like this:
//! Basic example of Rust in Android.
/// Prints a "Hello Android" to the terminal you call adb from.
fn main() {
println!("Hello Android");
}
Some things to note: the AOSP clippy configuration requires the comments in the above code. The first line is an outer documentation comment (//!
); it’s meant to describe what the purpose of the file is. The comment above the function (///
) is meant to document what the specific function does. When you’re writing libraries, you have to add these above every public function.
The first step in getting this running on the phone is to run m hello_android
which tells the Soong build system to build just your newly defined module. You can also cd into the directory that has your Android.bp
file and simply run mm
which will do the same thing. This will create an executable file inside of the out directory here: $ANDROID_PRODUCT_OUT/system/bin/hello_android
where ANDROID_PRODUCT_OUT
is out/target/product/vsoc_x86_64
, assuming you’re using the cuttlefish emulator.
With the executable now built on your development computer, you need to push it onto the device in order to run it. You can run adb push "$ANDROID_PRODUCT_OUT/system/bin/hello_android" /data/local/tmp
to get the executable in a temp directory on the emulator. To call it, just run adb shell /data/local/tmp/hello_android
. You should see “Hello Android” in the terminal.
Using AOSP Rust Libraries
Now technically, that’s an Android module written in Rust, but I want to go over an example where we pull in a library that already exists elsewhere in the AOSP. The Android build system is hermetic, ensuring that dependencies will continue working together across builds. If you attempt to bring in a new library, you have to make sure it will work with the dependency version that are already present in your build.
To check for existing libraries, you can navigate to the external/rust/crates/
directory. This folder contains all of the imported Rust libraries that are used within the project. Rust libraries are linked dynamically by default when you’re targeting a build for the emulated device. You can of course interop with C/C++ as well as Java libraries, but that will require a more dedicated post.
For fun, let’s write some basic SQL using the Rusqlite crate. Navigate to external/rust/crates/rusqlite
. As we mentioned before, all modules require an Android.bp
file, so go to the Android.bp
file inside of the rusqlite directory. This file tells us how we can call the library from our own module. You should see this library definition:
rust_library {
name: "librusqlite",
host_supported: true,
crate_name: "rusqlite",
cargo_env_compat: true,
cargo_pkg_version: "0.29.0",
srcs: ["src/lib.rs"],
edition: "2018",
features: [
"modern_sqlite",
"trace",
],
rustlibs: [
"libbitflags",
"libfallible_iterator",
"libfallible_streaming_iterator",
"libhashlink",
"liblibsqlite3_sys",
"libsmallvec",
],
apex_available: [
"//apex_available:platform",
"//apex_available:anyapex",
],
}
There are a few important things to note for our usage of this library. The first is the name
attribute: “librusqlite”. The second is the crate_name
: “rusqlite”. To use rusqlite in your hello-android
module, we will need to define a Rust library in our Android.bp file:
rust_library {
name: "libdbingest",
crate_name: "dbingest",
srcs: ["lib/db_ingest.rs"],
rustlibs: ["librusqlite"],
}
And to then use this new library in our binary:
rust_binary {
name: "hello_android",
crate_name: "hello_android",
srcs: ["src/main.rs"],
rustlibs: ["libdbingest"],
}
Now the crate_name
in rusqlite’s Android.bp
file comes into play when we get into importing it in our Rust code. Let’s use this library to read the SMS database.
Using Rusqlite to Read Private Databases
First, we should create a new directory inside of our hello-android
folder and call it lib
. cd lib
and create a file named db_ingest.rs
. We’re going to use the rusqlite client to read the phone’s SMS database.
We first import rusqlite and define the structure we want returned from the table as well as how to read the table:
//! Rust library to work with SMS messages from the Android SMS database.
use rusqlite::{params, Connection, Result};
/// Struct describing the columns to get from the SMS table.
pub struct SmsMessage {
/// Unique id of the SMS message.
pub id: i64,
/// Sender or recipient of the SMS message.
pub address: String,
/// Date the SMS message was sent.
pub date: i64,
/// The contents of the sms message.
pub body: String,
}
/// Struct to hold information about a database table to ingest.
pub struct IngestTable {
/// Where to begin the ingest.
pub cursor: i64,
/// Name of the table to ingest.
pub table_name: String,
/// File path of the database to ingest from.
pub database_file: String,
}
Note that everything that’s labeled as public needs to have documentation on it according to the clippy ruleset.
Next, we define a trait pattern for structs of databases that we can ingetst:
/// Trait to describe how to ingest a database table.
pub trait DbIngestible {
/// Get the select query for the table.
fn select_query(table_name: &str) -> String;
/// Create a struct from a database row.
fn from_row(row: &rusqlite::Row) -> rusqlite::Result<Self>
where
Self: Sized;
}
In the case of the SmsMessage
, we can define its select_query
and from_row
functions as so:
impl DbIngestible for SmsMessage {
fn select_query(table_name: &str, cursor: &Option<i64>) -> String {
let mut query = format!("SELECT _id, address, date, body FROM {}", table_name);
if let Some(cursor) = cursor {
query.push_str(format!(" WHERE date > {}", cursor).as_str());
}
query
}
fn from_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
Ok(SmsMessage {
id: row.get(0)?,
address: row.get(1)?,
date: row.get(2)?,
body: row.get(3)?,
})
}
}
The select_query
function is pretty straightforward; we’re simply generating a string to define the column values we would like to get back. If a cursor value is passed in, we append a WHERE clause to segment the results of the SMS table.
from_row
is also simple in that it’s just defining how we can convert the row iterator into a structured format that we can use elsewhere. The indices that we pass into the get
method correlate with the order in which they appear in our select
statement above.
We can bring this all together in an ingest
function defined as so:
/// Ingest database tables
pub fn ingest<T: DbIngestible>(ingest: IngestTable) -> Result<Vec<T>, Box<dyn std::error::Error>> {
let conn = Connection::open(ingest.database_file.as_str())?;
let select_query = T::select_query(&ingest.table_name, &ingest.cursor);
let mut stmt = conn.prepare(&select_query)?;
let items: Result<Vec<T>, _> = stmt.query_map([], T::from_row)?.collect();
Ok(items?)
}
This is a very straightforward function that just returns the query results. I modified this for simplicity-sake from my own codebase where the function paginates and embeds various private databases, so it inherited the “ingest” namesake.
The ingest
function is setup to accept the various parameters required for identifying the right table to read (derived from the IngestTable
struct); this includes the database path as well as the table name. It then returns a vector of generics that implement the DbIngestible trait. For now, we only have the SmsMessage struct, but you can imagine adapting this for the Gmail or WhatsApp databases as well.
We first get a connection by passing the database’s path as a &str
to Connection::open()
. We call our select_query
method for the given DbIngestible
we are passing in which returns the raw SQL string. We pass this into the prepare
method to convert it into a properly formatted SQL query, which we then execute by calling query_map
on the resulting Statement
of the prepare
method.
Finally, we execute the query with query_map
. The empty brackets signify that we aren’t passing any variables to our SQL query (these would replace ”?” in the raw query string). The second parameter is a function describing how to handle an individual row. In our case, it’s structuring it as a SmsMessage
in our from_row
implementation.
Putting It All Together
Let’s now integrate our new library with our existing main.rs
file. Since we already defined and included the library in our Android.bp
file, we can now simply use
it by referencing its crate_name
from the rust_library
definition. Our new src/main.rs
file will look like this:
//! Basic example of Rust in Android.
use dbingest::{ingest, IngestTable, SmsMessage};
/// Prints SMS message data from the SQLite database
fn main() {
let ingest_table = IngestTable {
cursor: None,
table_name: "sms".to_string(),
database_file: "/data/data/com.android.providers.telephony/databases/mmssms.db",
};
let sms_messages: Vec<SmsMessage> = ingest::<SmsMessage>(ingest_table).unwrap();
for message in sms_messages {
println!("{}", message.body);
}
}
Remember, your SMS database is likely empty assuming you’re using the emulator, so before trying to run this, you should populate it with some messages first.
Once you have some messages to test it on, you can again run m hello_android
to build the binary. This will automatically also build its dependencies. Then adb push "$ANDROID_PRODUCT_OUT/system/bin/hello_android" /data/local/tmp
followed by adb shell /data/local/tmp/hello_android
. Once you run the shell command, you will likely get an error about not having the proper permission to access this database. You can run adb root && adb remount
to restart the adb daemon with root permissions and try again after restarting the emulated device. Now you should be able to see all of the test SMS messages you’ve sent.