musikr: basic taglib rust shim
This commit is contained in:
parent
534f06d7e1
commit
729a3c3273
10 changed files with 647 additions and 26 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
165
musikr/src/main/jni/shim/iostream_shim.cpp
Normal file
165
musikr/src/main/jni/shim/iostream_shim.cpp
Normal 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
|
56
musikr/src/main/jni/shim/iostream_shim.hpp
Normal file
56
musikr/src/main/jni/shim/iostream_shim.hpp
Normal 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);
|
||||
|
||||
}
|
167
musikr/src/main/jni/src/jni_stream.rs
Normal file
167
musikr/src/main/jni/src/jni_stream.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
45
musikr/src/main/jni/src/taglib/ffi.rs
Normal file
45
musikr/src/main/jni/src/taglib/ffi.rs
Normal 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;
|
||||
}
|
||||
}
|
73
musikr/src/main/jni/src/taglib/mod.rs
Normal file
73
musikr/src/main/jni/src/taglib/mod.rs
Normal 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
|
||||
}
|
||||
}
|
94
musikr/src/main/jni/src/taglib/stream.rs
Normal file
94
musikr/src/main/jni/src/taglib/stream.rs
Normal 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()
|
||||
}
|
Loading…
Reference in a new issue