diff --git a/musikr/src/main/java/org/oxycblt/musikr/metadata/MetadataExtractor.kt b/musikr/src/main/java/org/oxycblt/musikr/metadata/MetadataExtractor.kt index 4b9fd9319..abf00fc3d 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/metadata/MetadataExtractor.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/metadata/MetadataExtractor.kt @@ -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 } diff --git a/musikr/src/main/java/org/oxycblt/musikr/metadata/MetadataJNI.kt b/musikr/src/main/java/org/oxycblt/musikr/metadata/MetadataJNI.kt index 715b57915..74aaa358a 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/metadata/MetadataJNI.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/metadata/MetadataJNI.kt @@ -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 } diff --git a/musikr/src/main/jni/build.rs b/musikr/src/main/jni/build.rs index 8ff16d904..6c446502c 100644 --- a/musikr/src/main/jni/build.rs +++ b/musikr/src/main/jni/build.rs @@ -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"); } diff --git a/musikr/src/main/jni/shim/iostream_shim.cpp b/musikr/src/main/jni/shim/iostream_shim.cpp new file mode 100644 index 000000000..e48b4cbf7 --- /dev/null +++ b/musikr/src/main/jni/shim/iostream_shim.cpp @@ -0,0 +1,165 @@ +#include "iostream_shim.hpp" +#include +#include + +// 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 new_rust_iostream(RustStream* stream) { + return std::unique_ptr(new RustIOStream(stream)); +} + +// Factory function to create a FileRef from a stream +std::unique_ptr new_FileRef_from_stream(std::unique_ptr stream) { + return std::make_unique(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 buffer(length); + size_t bytes_read = rust_stream_read(rust_stream, buffer.data(), length); + return TagLib::ByteVector(reinterpret_cast(buffer.data()), bytes_read); +} + +void RustIOStream::writeBlock(const TagLib::ByteVector& data) { + rust_stream_write(rust_stream, + reinterpret_cast(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 \ No newline at end of file diff --git a/musikr/src/main/jni/shim/iostream_shim.hpp b/musikr/src/main/jni/shim/iostream_shim.hpp new file mode 100644 index 000000000..2826057a2 --- /dev/null +++ b/musikr/src/main/jni/shim/iostream_shim.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +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 new_rust_iostream(RustStream* stream); +std::unique_ptr new_FileRef_from_stream(std::unique_ptr 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); + +} \ No newline at end of file diff --git a/musikr/src/main/jni/src/jni_stream.rs b/musikr/src/main/jni/src/jni_stream.rs new file mode 100644 index 000000000..58bffada6 --- /dev/null +++ b/musikr/src/main/jni/src/jni_stream.rs @@ -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 { + // 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 { + // 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 { + 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 { + 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) + } +} \ No newline at end of file diff --git a/musikr/src/main/jni/src/lib.rs b/musikr/src/main/jni/src/lib.rs index 147a0a6f2..e1c7f8e76 100644 --- a/musikr/src/main/jni/src/lib.rs +++ b/musikr/src/main/jni/src/lib.rs @@ -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() } diff --git a/musikr/src/main/jni/src/taglib/ffi.rs b/musikr/src/main/jni/src/taglib/ffi.rs new file mode 100644 index 000000000..b8005dea7 --- /dev/null +++ b/musikr/src/main/jni/src/taglib/ffi.rs @@ -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; + #[namespace = "taglib_shim"] + fn new_FileRef_from_stream(stream: UniquePtr) -> UniquePtr; + + // 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; + } +} diff --git a/musikr/src/main/jni/src/taglib/mod.rs b/musikr/src/main/jni/src/taglib/mod.rs new file mode 100644 index 000000000..8001deb94 --- /dev/null +++ b/musikr/src/main/jni/src/taglib/mod.rs @@ -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, +} + +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 { + // 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 + } +} \ No newline at end of file diff --git a/musikr/src/main/jni/src/taglib/stream.rs b/musikr/src/main/jni/src/taglib/stream.rs new file mode 100644 index 000000000..80b6b1366 --- /dev/null +++ b/musikr/src/main/jni/src/taglib/stream.rs @@ -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); + +impl<'a> RustStream<'a> { + pub fn new(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() +} \ No newline at end of file