musikr: basic taglib rust shim

This commit is contained in:
Alexander Capehart 2025-02-08 09:55:46 -07:00
parent 534f06d7e1
commit 729a3c3273
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
10 changed files with 647 additions and 26 deletions

View file

@ -37,7 +37,8 @@ private object MetadataExtractorImpl : MetadataExtractor {
override suspend fun extract(deviceFile: DeviceFile, fd: ParcelFileDescriptor) =
withContext(Dispatchers.IO) {
val fis = FileInputStream(fd.fileDescriptor)
Log.d("MetadataExtractorImpl", MetadataJNI.rust("bruh"))
val input = NativeInputStream(deviceFile, fis)
Log.d("MetadataExtractorImpl", MetadataJNI.openFile(input))
// MetadataJNI.open(deviceFile, fis).also { fis.close() }
null
}

View file

@ -25,5 +25,5 @@ internal object MetadataJNI {
// This is a rust function, Android Studio has no idea how to link to it
@Suppress("KotlinJniMissingFunction")
external fun rust(a: String): String
external fun openFile(input: NativeInputStream): String
}

View file

@ -106,8 +106,17 @@ fn main() {
println!("cargo:rustc-link-search=native={}/lib", arch_pkg_dir.display());
println!("cargo:rustc-link-lib=static=tag");
cxx_build::bridge("src/lib.rs")
.include(format!["taglib/pkg/{}/include", arch])
// Build the shim and cxx bridge together
cxx_build::bridge("src/taglib/ffi.rs")
.file("shim/iostream_shim.cpp")
.include(format!("taglib/pkg/{}/include", arch))
.include("shim")
.include(".") // Add the current directory to include path
.flag_if_supported("-std=c++14")
.compile("taglib_cxx_bindings");
// Rebuild if shim files change
println!("cargo:rerun-if-changed=shim/iostream_shim.hpp");
println!("cargo:rerun-if-changed=shim/iostream_shim.cpp");
println!("cargo:rerun-if-changed=src/taglib/ffi.rs");
}

View file

@ -0,0 +1,165 @@
#include "iostream_shim.hpp"
#include <stdexcept>
#include <rust/cxx.h>
// These are the functions we'll define in Rust
extern "C" {
const char* rust_stream_name(const void* stream);
size_t rust_stream_read(void* stream, uint8_t* buffer, size_t length);
void rust_stream_write(void* stream, const uint8_t* data, size_t length);
void rust_stream_seek(void* stream, int64_t offset, int32_t whence);
void rust_stream_truncate(void* stream, int64_t length);
int64_t rust_stream_tell(const void* stream);
int64_t rust_stream_length(const void* stream);
bool rust_stream_is_readonly(const void* stream);
}
namespace taglib_shim {
// Factory function to create a new RustIOStream
std::unique_ptr<RustIOStream> new_rust_iostream(RustStream* stream) {
return std::unique_ptr<RustIOStream>(new RustIOStream(stream));
}
// Factory function to create a FileRef from a stream
std::unique_ptr<TagLib::FileRef> new_FileRef_from_stream(std::unique_ptr<RustIOStream> stream) {
return std::make_unique<TagLib::FileRef>(stream.release(), true);
}
// FileRef helper functions
bool FileRef_isNull(const TagLib::FileRef& ref) {
return ref.isNull();
}
const TagLib::File& FileRef_file(const TagLib::FileRef& ref) {
return *ref.file();
}
// File tag methods
bool File_tag(const TagLib::File& file) {
return file.tag() != nullptr;
}
namespace {
// Keep the empty string as a static member to ensure it lives long enough
const TagLib::String empty_string;
}
const TagLib::String& File_tag_title(const TagLib::File& file) {
if (auto* tag = file.tag()) {
static TagLib::String title;
title = tag->title();
return title;
}
return empty_string;
}
// String utilities
const char* to_string(const TagLib::String& str) {
return str.toCString(true);
}
bool isEmpty(const TagLib::String& str) {
return str.isEmpty();
}
RustIOStream::RustIOStream(RustStream* stream) : rust_stream(stream) {}
RustIOStream::~RustIOStream() = default;
TagLib::FileName RustIOStream::name() const {
return rust_stream_name(rust_stream);
}
TagLib::ByteVector RustIOStream::readBlock(size_t length) {
std::vector<uint8_t> buffer(length);
size_t bytes_read = rust_stream_read(rust_stream, buffer.data(), length);
return TagLib::ByteVector(reinterpret_cast<char*>(buffer.data()), bytes_read);
}
void RustIOStream::writeBlock(const TagLib::ByteVector& data) {
rust_stream_write(rust_stream,
reinterpret_cast<const uint8_t*>(data.data()),
data.size());
}
void RustIOStream::insert(const TagLib::ByteVector& data, TagLib::offset_t start, size_t replace) {
// Save current position
auto current = tell();
// Seek to insert position
seek(start);
// If replacing, remove that section first
if (replace > 0) {
removeBlock(start, replace);
}
// Write new data
writeBlock(data);
// Restore position
seek(current);
}
void RustIOStream::removeBlock(TagLib::offset_t start, size_t length) {
if (length == 0) return;
// Save current position
auto current = tell();
// Get file size
auto file_length = this->length();
// Read everything after the removed section
seek(start + length);
auto remaining = readBlock(file_length - (start + length));
// Truncate to start position
seek(start);
truncate(start);
// Write remaining data
writeBlock(remaining);
// Restore position
seek(current);
}
void RustIOStream::seek(TagLib::offset_t offset, Position p) {
int32_t whence;
switch (p) {
case Beginning: whence = SEEK_SET; break;
case Current: whence = SEEK_CUR; break;
case End: whence = SEEK_END; break;
default: throw std::runtime_error("Invalid seek position");
}
rust_stream_seek(rust_stream, offset, whence);
}
void RustIOStream::clear() {
truncate(0);
seek(0);
}
void RustIOStream::truncate(TagLib::offset_t length) {
rust_stream_truncate(rust_stream, length);
}
TagLib::offset_t RustIOStream::tell() const {
return rust_stream_tell(rust_stream);
}
TagLib::offset_t RustIOStream::length() {
return rust_stream_length(rust_stream);
}
bool RustIOStream::readOnly() const {
return rust_stream_is_readonly(rust_stream);
}
bool RustIOStream::isOpen() const {
return true; // If we have a stream, it's open
}
} // namespace taglib_shim

View file

@ -0,0 +1,56 @@
#pragma once
#include <memory>
#include <string>
#include <taglib/tiostream.h>
#include <taglib/fileref.h>
#include <taglib/tag.h>
#include <taglib/tstring.h>
#include <cstdint>
namespace taglib_shim {
// Forward declaration of the Rust-side stream
struct RustStream;
// C++ implementation of TagLib::IOStream that delegates to Rust
class RustIOStream : public TagLib::IOStream {
public:
explicit RustIOStream(RustStream* stream);
~RustIOStream() override;
// TagLib::IOStream interface implementation
TagLib::FileName name() const override;
TagLib::ByteVector readBlock(size_t length) override;
void writeBlock(const TagLib::ByteVector& data) override;
void insert(const TagLib::ByteVector& data, TagLib::offset_t start = 0, size_t replace = 0) override;
void removeBlock(TagLib::offset_t start = 0, size_t length = 0) override;
void seek(TagLib::offset_t offset, Position p = Beginning) override;
void clear() override;
void truncate(TagLib::offset_t length) override;
TagLib::offset_t tell() const override;
TagLib::offset_t length() override;
bool readOnly() const override;
bool isOpen() const override;
private:
RustStream* rust_stream;
};
// Factory functions
std::unique_ptr<RustIOStream> new_rust_iostream(RustStream* stream);
std::unique_ptr<TagLib::FileRef> new_FileRef_from_stream(std::unique_ptr<RustIOStream> stream);
// FileRef helper functions
bool FileRef_isNull(const TagLib::FileRef& ref);
const TagLib::File& FileRef_file(const TagLib::FileRef& ref);
// File tag methods
bool File_tag(const TagLib::File& file);
const TagLib::String& File_tag_title(const TagLib::File& file);
// String utilities
const char* to_string(const TagLib::String& str);
bool isEmpty(const TagLib::String& str);
}

View file

@ -0,0 +1,167 @@
use std::io::{Read, Seek, SeekFrom, Write};
use jni::objects::{JObject, JValue};
use jni::JNIEnv;
use crate::taglib::TagLibStream;
pub struct JInputStream<'local, 'a> {
env: &'a mut JNIEnv<'local>,
input: JObject<'local>,
// Cache the method IDs
name_method: jni::objects::JMethodID,
read_block_method: jni::objects::JMethodID,
is_open_method: jni::objects::JMethodID,
seek_from_beginning_method: jni::objects::JMethodID,
seek_from_current_method: jni::objects::JMethodID,
seek_from_end_method: jni::objects::JMethodID,
tell_method: jni::objects::JMethodID,
length_method: jni::objects::JMethodID,
}
impl<'local, 'a> JInputStream<'local, 'a> {
pub fn new(env: &'a mut JNIEnv<'local>, input: JObject<'local>) -> Result<Self, jni::errors::Error> {
// Get the class reference
let class = env.find_class("org/oxycblt/musikr/metadata/NativeInputStream")?;
// Cache all method IDs
Ok(JInputStream {
name_method: env.get_method_id(&class, "name", "()Ljava/lang/String;")?,
read_block_method: env.get_method_id(&class, "readBlock", "(Ljava/nio/ByteBuffer;)Z")?,
is_open_method: env.get_method_id(&class, "isOpen", "()Z")?,
seek_from_beginning_method: env.get_method_id(&class, "seekFromBeginning", "(J)Z")?,
seek_from_current_method: env.get_method_id(&class, "seekFromCurrent", "(J)Z")?,
seek_from_end_method: env.get_method_id(&class, "seekFromEnd", "(J)Z")?,
tell_method: env.get_method_id(&class, "tell", "()J")?,
length_method: env.get_method_id(&class, "length", "()J")?,
env,
input,
})
}
}
impl<'local, 'a> TagLibStream for JInputStream<'local, 'a> {
fn name(&mut self) -> String {
// Call the Java name() method
let name = unsafe {
self.env
.call_method_unchecked(
&self.input,
self.name_method,
jni::signature::ReturnType::Object,
&[]
)
.unwrap()
.l()
.unwrap()
};
self.env
.get_string(&name.into())
.unwrap()
.into()
}
fn is_readonly(&self) -> bool {
true // JInputStream is always read-only
}
}
impl<'local, 'a> Read for JInputStream<'local, 'a> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
// Create a direct ByteBuffer from the Rust slice
let byte_buffer = unsafe {
self.env
.new_direct_byte_buffer(buf.as_mut_ptr(), buf.len())
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?
};
// Call readBlock
let success = unsafe {
self.env
.call_method_unchecked(
&self.input,
self.read_block_method,
jni::signature::ReturnType::Primitive(jni::signature::Primitive::Boolean),
&[JValue::Object(&byte_buffer).as_jni()]
)
.unwrap()
.z()
.unwrap()
};
if !success {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"Failed to read block"
));
}
Ok(buf.len())
}
}
impl<'local, 'a> Write for JInputStream<'local, 'a> {
fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"JInputStream is read-only"
))
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(()) // Nothing to flush in a read-only stream
}
}
impl<'local, 'a> Seek for JInputStream<'local, 'a> {
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
let (method, offset) = match pos {
SeekFrom::Start(offset) => (self.seek_from_beginning_method, offset as i64),
SeekFrom::Current(offset) => (self.seek_from_current_method, offset),
SeekFrom::End(offset) => (self.seek_from_end_method, offset),
};
// Call the appropriate seek method
let success = unsafe {
self.env
.call_method_unchecked(
&self.input,
method,
jni::signature::ReturnType::Primitive(jni::signature::Primitive::Boolean),
&[JValue::Long(offset).as_jni()]
)
.unwrap()
.z()
.unwrap()
};
if !success {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"Failed to seek"
));
}
// Return current position
let position = unsafe {
self.env
.call_method_unchecked(
&self.input,
self.tell_method,
jni::signature::ReturnType::Primitive(jni::signature::Primitive::Long),
&[]
)
.unwrap()
.j()
.unwrap()
};
if position == i64::MIN {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"Failed to get position"
));
}
Ok(position as u64)
}
}

View file

@ -1,33 +1,44 @@
use jni::objects::{JClass, JString};
use jni::objects::{JClass, JObject};
use jni::sys::jstring;
use jni::JNIEnv;
#[cxx::bridge]
mod ffi {
unsafe extern "C++" {
include!("taglib/taglib.h");
mod taglib;
mod jni_stream;
type FileRef;
type File;
}
}
pub use taglib::*;
use jni_stream::JInputStream;
#[no_mangle]
pub extern "C" fn Java_org_oxycblt_musikr_metadata_MetadataJNI_rust(
mut env: JNIEnv,
_class: JClass,
input: JString,
pub extern "system" fn Java_HelloWorld_hello<'local>(
mut env: JNIEnv<'local>,
_class: JClass<'local>,
input: JObject<'local>,
) -> jstring {
// Convert the Java string (JString) to a Rust string
let input: String = env.get_string(&input).expect("Couldn't get java string!").into();
// Create JInputStream from the Java input stream
let stream = match JInputStream::new(&mut env, input) {
Ok(stream) => stream,
Err(e) => {
let error = format!("Failed to create input stream: {}", e);
let error_str = env.new_string(error).expect("Couldn't create error string!");
return error_str.into_raw();
}
};
// Process the input string (this is where your Rust logic would go)
let output = format!("Hello from Rust, {}", input);
// Create FileRef from the stream
let file_ref = match FileRef::from_stream(stream) {
Some(file_ref) => file_ref,
None => {
let error = "Failed to create FileRef";
let error_str = env.new_string(error).expect("Couldn't create error string!");
return error_str.into_raw();
}
};
// Convert the Rust string back to a Java string (jstring)
let output = env.new_string(output).expect("Couldn't create java string!");
// Return the Java string
// Get the file and read the title
let file = file_ref.file();
let title = file.title().unwrap_or("No title");
// Return the title
let output = env.new_string(title).expect("Couldn't create string!");
output.into_raw()
}

View file

@ -0,0 +1,45 @@
#[cxx::bridge]
pub(crate) mod bindings {
unsafe extern "C++" {
include!("taglib/taglib.h");
include!("taglib/tstring.h");
include!("shim/iostream_shim.hpp");
#[namespace = "TagLib"]
type FileRef;
#[namespace = "TagLib"]
type File;
#[namespace = "TagLib"]
#[cxx_name = "String"]
type TagString;
#[namespace = "taglib_shim"]
type RustIOStream;
#[namespace = "taglib_shim"]
type RustStream;
// Create a FileRef from an iostream
#[namespace = "taglib_shim"]
unsafe fn new_rust_iostream(stream: *mut RustStream) -> UniquePtr<RustIOStream>;
#[namespace = "taglib_shim"]
fn new_FileRef_from_stream(stream: UniquePtr<RustIOStream>) -> UniquePtr<FileRef>;
// FileRef helper functions
#[namespace = "taglib_shim"]
fn FileRef_isNull(ref_: &FileRef) -> bool;
#[namespace = "taglib_shim"]
fn FileRef_file(ref_: &FileRef) -> &File;
// File tag methods
#[namespace = "taglib_shim"]
fn File_tag(file: &File) -> bool;
#[namespace = "taglib_shim"]
fn File_tag_title(file: &File) -> &TagString;
// String conversion utilities
#[namespace = "taglib_shim"]
unsafe fn to_string(s: &TagString) -> *const c_char;
#[namespace = "taglib_shim"]
fn isEmpty(s: &TagString) -> bool;
}
}

View file

@ -0,0 +1,73 @@
mod ffi;
mod stream;
pub use stream::{RustStream, TagLibStream};
use ffi::bindings;
// Store extracted tag data instead of C++ reference
#[derive(Default)]
pub struct File {
title: Option<String>,
}
impl File {
/// Get the title of the file, if available
pub fn title(&self) -> Option<&str> {
self.title.as_deref()
}
}
// Safe wrapper for FileRef that owns extracted data
pub struct FileRef {
file: File,
}
impl FileRef {
/// Create a new FileRef from a stream implementing TagLibStream
pub fn from_stream<'a, T: TagLibStream + 'a>(stream: T) -> Option<Self> {
// Create the RustStream wrapper
let rust_stream = stream::RustStream::new(stream);
// Convert to raw pointer for FFI
let raw_stream = Box::into_raw(Box::new(rust_stream)) as *mut bindings::RustStream;
// Create the RustIOStream C++ wrapper
let iostream = unsafe { ffi::bindings::new_rust_iostream(raw_stream) };
// Create FileRef from iostream
let inner = ffi::bindings::new_FileRef_from_stream(iostream);
if ffi::bindings::FileRef_isNull(&inner) {
return None;
}
// Extract data from C++ objects
let file_ref = &inner;
let file_ptr = ffi::bindings::FileRef_file(&file_ref);
// Extract title
let title = {
let title = ffi::bindings::File_tag_title(file_ptr);
if ffi::bindings::isEmpty(title) {
None
} else {
let cstr = unsafe { ffi::bindings::to_string(title) };
unsafe { std::ffi::CStr::from_ptr(cstr) }
.to_str()
.ok()
.map(|s| s.to_owned())
}
};
// Clean up C++ objects - they will be dropped when inner is dropped
drop(inner);
// Create File with extracted data
let file = File { title };
Some(FileRef { file })
}
pub fn file(&self) -> &File {
&self.file
}
}

View file

@ -0,0 +1,94 @@
use std::ffi::{c_void, CString};
use std::io::{Read, Seek, SeekFrom, Write};
use std::os::raw::c_char;
// Trait that must be implemented by Rust streams to be used with TagLib
pub trait TagLibStream: Read + Write + Seek {
fn name(&mut self) -> String;
fn is_readonly(&self) -> bool;
}
// Opaque type for C++
#[repr(C)]
pub struct RustStream<'a>(Box<dyn TagLibStream + 'a>);
impl<'a> RustStream<'a> {
pub fn new<T: TagLibStream + 'a>(stream: T) -> Self {
RustStream(Box::new(stream))
}
}
#[no_mangle]
pub extern "C" fn rust_stream_name(stream: *mut c_void) -> *const c_char {
let stream = unsafe { &mut *(stream as *mut RustStream<'_>) };
let name = stream.0.name();
// Note: This leaks memory, but TagLib only calls this once during construction
// and keeps the pointer, so it's fine
CString::new(name).unwrap().into_raw()
}
#[no_mangle]
pub extern "C" fn rust_stream_read(
stream: *mut c_void,
buffer: *mut u8,
length: usize,
) -> usize {
let stream = unsafe { &mut *(stream as *mut RustStream<'_>) };
let buffer = unsafe { std::slice::from_raw_parts_mut(buffer, length) };
stream.0.read(buffer).unwrap_or(0)
}
#[no_mangle]
pub extern "C" fn rust_stream_write(
stream: *mut c_void,
data: *const u8,
length: usize,
) {
let stream = unsafe { &mut *(stream as *mut RustStream<'_>) };
let data = unsafe { std::slice::from_raw_parts(data, length) };
stream.0.write_all(data).unwrap();
}
#[no_mangle]
pub extern "C" fn rust_stream_seek(
stream: *mut c_void,
offset: i64,
whence: i32,
) {
let stream = unsafe { &mut *(stream as *mut RustStream<'_>) };
let pos = match whence {
0 => SeekFrom::Start(offset as u64),
1 => SeekFrom::Current(offset),
2 => SeekFrom::End(offset),
_ => panic!("Invalid seek whence"),
};
stream.0.seek(pos).unwrap();
}
#[no_mangle]
pub extern "C" fn rust_stream_truncate(stream: *mut c_void, length: i64) {
let stream = unsafe { &mut *(stream as *mut RustStream<'_>) };
stream.0.seek(SeekFrom::Start(length as u64)).unwrap();
// TODO: Actually implement truncate once we have a better trait bound
}
#[no_mangle]
pub extern "C" fn rust_stream_tell(stream: *mut c_void) -> i64 {
let stream = unsafe { &mut *(stream as *mut RustStream<'_>) };
stream.0.seek(SeekFrom::Current(0)).unwrap() as i64
}
#[no_mangle]
pub extern "C" fn rust_stream_length(stream: *mut c_void) -> i64 {
let stream = unsafe { &mut *(stream as *mut RustStream<'_>) };
let current = stream.0.seek(SeekFrom::Current(0)).unwrap();
let end = stream.0.seek(SeekFrom::End(0)).unwrap();
stream.0.seek(SeekFrom::Start(current)).unwrap();
end as i64
}
#[no_mangle]
pub extern "C" fn rust_stream_is_readonly(stream: *const c_void) -> bool {
let stream = unsafe { &*(stream as *const RustStream<'_>) };
stream.0.is_readonly()
}