musikr: start putting unsafe stuff into ffi mod

Aiming for like 3 layers of abstraction:
Layer 1: Top-level taglib-esque API translated to jni
Layer 2: Slightly extended unsafe wrappers over bindings
Level 3: Raw taglib bindings and shims
This commit is contained in:
Alexander Capehart 2025-02-08 21:42:39 -07:00
parent 3aa39a7065
commit 289582964c
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
4 changed files with 277 additions and 173 deletions

View file

@ -2,37 +2,40 @@
namespace taglib_shim { namespace taglib_shim {
// File conversion functions const TagLib::Ogg::File* File_asOgg(const TagLib::File* file) {
TagLib::Ogg::Vorbis::File* File_asVorbis(TagLib::File* file) { return dynamic_cast<const TagLib::Ogg::File*>(file);
return dynamic_cast<TagLib::Ogg::Vorbis::File*>(file);
} }
TagLib::Ogg::Opus::File* File_asOpus(TagLib::File* file) { const TagLib::Ogg::Vorbis::File* File_asVorbis(const TagLib::File* file) {
return dynamic_cast<TagLib::Ogg::Opus::File*>(file); return dynamic_cast<const TagLib::Ogg::Vorbis::File*>(file);
} }
TagLib::MPEG::File* File_asMPEG(TagLib::File* file) { const TagLib::Ogg::Opus::File* File_asOpus(const TagLib::File* file) {
return dynamic_cast<TagLib::MPEG::File*>(file); return dynamic_cast<const TagLib::Ogg::Opus::File*>(file);
} }
TagLib::FLAC::File* File_asFLAC(TagLib::File* file) { const TagLib::MPEG::File* File_asMPEG(const TagLib::File* file) {
return dynamic_cast<TagLib::FLAC::File*>(file); return dynamic_cast<const TagLib::MPEG::File*>(file);
} }
TagLib::MP4::File* File_asMP4(TagLib::File* file) { const TagLib::FLAC::File* File_asFLAC(const TagLib::File* file) {
return dynamic_cast<TagLib::MP4::File*>(file); return dynamic_cast<const TagLib::FLAC::File*>(file);
} }
TagLib::RIFF::WAV::File* File_asWAV(TagLib::File* file) { const TagLib::MP4::File* File_asMP4(const TagLib::File* file) {
return dynamic_cast<TagLib::RIFF::WAV::File*>(file); return dynamic_cast<const TagLib::MP4::File*>(file);
} }
TagLib::WavPack::File* File_asWavPack(TagLib::File* file) { const TagLib::RIFF::WAV::File* File_asWAV(const TagLib::File* file) {
return dynamic_cast<TagLib::WavPack::File*>(file); return dynamic_cast<const TagLib::RIFF::WAV::File*>(file);
} }
TagLib::APE::File* File_asAPE(TagLib::File* file) { const TagLib::WavPack::File* File_asWavPack(const TagLib::File* file) {
return dynamic_cast<TagLib::APE::File*>(file); return dynamic_cast<const TagLib::WavPack::File*>(file);
}
const TagLib::APE::File* File_asAPE(const TagLib::File* file) {
return dynamic_cast<const TagLib::APE::File*>(file);
} }
} // namespace taglib_shim } // namespace taglib_shim

View file

@ -17,13 +17,14 @@
namespace taglib_shim { namespace taglib_shim {
// File conversion functions // File conversion functions
TagLib::Ogg::Vorbis::File* File_asVorbis(TagLib::File* file); const TagLib::Ogg::File* File_asOgg(const TagLib::File* file);
TagLib::Ogg::Opus::File* File_asOpus(TagLib::File* file); const TagLib::Ogg::Vorbis::File* File_asVorbis(const TagLib::File* file);
TagLib::MPEG::File* File_asMPEG(TagLib::File* file); const TagLib::Ogg::Opus::File* File_asOpus(const TagLib::File* file);
TagLib::FLAC::File* File_asFLAC(TagLib::File* file); const TagLib::MPEG::File* File_asMPEG(const TagLib::File* file);
TagLib::MP4::File* File_asMP4(TagLib::File* file); const TagLib::FLAC::File* File_asFLAC(const TagLib::File* file);
TagLib::RIFF::WAV::File* File_asWAV(TagLib::File* file); const TagLib::MP4::File* File_asMP4(const TagLib::File* file);
TagLib::WavPack::File* File_asWavPack(TagLib::File* file); const TagLib::RIFF::WAV::File* File_asWAV(const TagLib::File* file);
TagLib::APE::File* File_asAPE(TagLib::File* file); const TagLib::WavPack::File* File_asWavPack(const TagLib::File* file);
const TagLib::APE::File* File_asAPE(const TagLib::File* file);
} // namespace taglib_shim } // namespace taglib_shim

View file

@ -1,3 +1,8 @@
use std::ffi::CStr;
use std::pin::Pin;
use std::string::ToString;
use std::collections::HashMap;
#[cxx::bridge] #[cxx::bridge]
pub(crate) mod bindings { pub(crate) mod bindings {
unsafe extern "C++" { unsafe extern "C++" {
@ -13,7 +18,7 @@ pub(crate) mod bindings {
#[namespace = "TagLib"] #[namespace = "TagLib"]
type FileRef; type FileRef;
fn isNull(self: Pin<&FileRef>) -> bool; fn isNull(self: Pin<&FileRef>) -> bool;
fn file(self: Pin<&FileRef>) -> *mut File; fn file(self: Pin<&FileRef>) -> *mut BaseFile;
#[namespace = "taglib_shim"] #[namespace = "taglib_shim"]
type RustIOStream; type RustIOStream;
@ -27,8 +32,9 @@ pub(crate) mod bindings {
fn new_FileRef_from_stream(stream: UniquePtr<RustIOStream>) -> UniquePtr<FileRef>; fn new_FileRef_from_stream(stream: UniquePtr<RustIOStream>) -> UniquePtr<FileRef>;
#[namespace = "TagLib"] #[namespace = "TagLib"]
type File; #[cxx_name = "File"]
fn audioProperties(self: Pin<&File>) -> *mut AudioProperties; type BaseFile;
fn audioProperties(self: Pin<&BaseFile>) -> *mut AudioProperties;
#[namespace = "TagLib"] #[namespace = "TagLib"]
type AudioProperties; type AudioProperties;
@ -37,29 +43,33 @@ pub(crate) mod bindings {
fn sampleRate(self: Pin<&AudioProperties>) -> i32; fn sampleRate(self: Pin<&AudioProperties>) -> i32;
fn channels(self: Pin<&AudioProperties>) -> i32; fn channels(self: Pin<&AudioProperties>) -> i32;
#[namespace = "TagLib::Ogg::Vorbis"] #[namespace = "TagLib::Ogg"]
#[cxx_name = "File"] #[cxx_name = "File"]
type VorbisFile; type OggFile;
unsafe fn tag(self: Pin<&VorbisFile>) -> *mut XiphComment;
#[namespace = "TagLib::FLAC"]
#[cxx_name = "File"]
type FLACFile;
#[namespace = "TagLib::Ogg::Opus"]
#[cxx_name = "File"]
type OpusFile;
unsafe fn tag(self: Pin<&OpusFile>) -> *mut XiphComment;
#[namespace = "TagLib::Ogg"] #[namespace = "TagLib::Ogg"]
type XiphComment; type XiphComment;
unsafe fn fieldListMap(self: Pin<&XiphComment>) -> &SimplePropertyMap; unsafe fn fieldListMap(self: Pin<&XiphComment>) -> &SimplePropertyMap;
#[namespace = "TagLib::Ogg::Vorbis"]
#[cxx_name = "File"]
type VorbisFile;
#[cxx_name = "tag"]
unsafe fn tag(self: Pin<&VorbisFile>) -> *mut XiphComment;
#[namespace = "TagLib::Ogg::Opus"]
#[cxx_name = "File"]
type OpusFile;
#[cxx_name = "tag"]
unsafe fn opusTag(self: Pin<&OpusFile>) -> *mut XiphComment;
#[namespace = "TagLib::FLAC"]
#[cxx_name = "File"]
type FLACFile;
#[namespace = "TagLib::MPEG"] #[namespace = "TagLib::MPEG"]
#[cxx_name = "File"] #[cxx_name = "File"]
type MPEGFile; type MPEGFile;
#[namespace = "TagLib::MP4"] #[namespace = "TagLib::MP4"]
#[cxx_name = "File"] #[cxx_name = "File"]
type MP4File; type MP4File;
@ -78,26 +88,30 @@ pub(crate) mod bindings {
// File conversion functions // File conversion functions
#[namespace = "taglib_shim"] #[namespace = "taglib_shim"]
unsafe fn File_asVorbis(file: *mut File) -> *mut VorbisFile; unsafe fn File_asOgg(file: *const BaseFile) -> *const OggFile;
#[namespace = "taglib_shim"] #[namespace = "taglib_shim"]
unsafe fn File_asOpus(file: *mut File) -> *mut OpusFile; unsafe fn File_asVorbis(file: *const BaseFile) -> *const VorbisFile;
#[namespace = "taglib_shim"] #[namespace = "taglib_shim"]
unsafe fn File_asMPEG(file: *mut File) -> *mut MPEGFile; unsafe fn File_asOpus(file: *const BaseFile) -> *const OpusFile;
#[namespace = "taglib_shim"] #[namespace = "taglib_shim"]
unsafe fn File_asFLAC(file: *mut File) -> *mut FLACFile; unsafe fn File_asMPEG(file: *const BaseFile) -> *const MPEGFile;
#[namespace = "taglib_shim"] #[namespace = "taglib_shim"]
unsafe fn File_asMP4(file: *mut File) -> *mut MP4File; unsafe fn File_asFLAC(file: *const BaseFile) -> *const FLACFile;
#[namespace = "taglib_shim"] #[namespace = "taglib_shim"]
unsafe fn File_asWAV(file: *mut File) -> *mut WAVFile; unsafe fn File_asMP4(file: *const BaseFile) -> *const MP4File;
#[namespace = "taglib_shim"] #[namespace = "taglib_shim"]
unsafe fn File_asWavPack(file: *mut File) -> *mut WavPackFile; unsafe fn File_asWAV(file: *const BaseFile) -> *const WAVFile;
#[namespace = "taglib_shim"] #[namespace = "taglib_shim"]
unsafe fn File_asAPE(file: *mut File) -> *mut APEFile; unsafe fn File_asWavPack(file: *const BaseFile) -> *const WavPackFile;
#[namespace = "taglib_shim"]
unsafe fn File_asAPE(file: *const BaseFile) -> *const APEFile;
#[namespace = "TagLib"] #[namespace = "TagLib"]
type SimplePropertyMap; type SimplePropertyMap;
#[namespace = "taglib_shim"] #[namespace = "taglib_shim"]
fn SimplePropertyMap_to_vector(field_list_map: Pin<&SimplePropertyMap>) -> UniquePtr<CxxVector<Property>>; fn SimplePropertyMap_to_vector(
field_list_map: Pin<&SimplePropertyMap>,
) -> UniquePtr<CxxVector<Property>>;
#[namespace = "taglib_shim"] #[namespace = "taglib_shim"]
type Property; type Property;
@ -114,7 +128,186 @@ pub(crate) mod bindings {
type TagString; type TagString;
#[namespace = "taglib_shim"] #[namespace = "taglib_shim"]
unsafe fn toCString(self: Pin<&TagString>, unicode: bool) -> *const c_char; unsafe fn toCString(self: Pin<&TagString>, unicode: bool) -> *const c_char;
#[namespace = "taglib_shim"] }
fn isEmpty(self: Pin<&TagString>) -> bool; }
impl bindings::FileRef {
pub fn file_or(&self) -> Option<&bindings::BaseFile> {
unsafe {
// SAFETY: This pin only lasts for the scope of this function.
// Nothing that can change the memory address of self is returned,
// only the address of the file pointer.
let pinned_self = Pin::new_unchecked(&*self);
if !pinned_self.isNull() {
pinned_self.file().as_ref()
} else {
None
}
}
}
}
impl bindings::BaseFile {
pub fn audio_properties(&self) -> Option<&bindings::AudioProperties> {
let props = unsafe {
let pinned_self = Pin::new_unchecked(self);
pinned_self.audioProperties()
};
unsafe {
props.as_ref()
}
}
pub fn as_opus(&self) -> Option<&bindings::OpusFile> {
let opus_file = unsafe {
bindings::File_asOpus(self as *const Self)
};
unsafe {
opus_file.as_ref()
}
}
pub fn as_vorbis(&self) -> Option<&bindings::VorbisFile> {
let vorbis_file = unsafe {
bindings::File_asVorbis(self as *const Self)
};
unsafe {
vorbis_file.as_ref()
}
}
}
impl bindings::AudioProperties {
pub fn length_ms(&self) -> i32 {
unsafe {
let pinned_self = Pin::new_unchecked(self);
pinned_self.lengthInMilliseconds()
}
}
pub fn bitrate_kbps(&self) -> i32 {
unsafe {
let pinned_self = Pin::new_unchecked(self);
pinned_self.bitrate()
}
}
pub fn sample_rate_hz(&self) -> i32 {
unsafe {
let pinned_self = Pin::new_unchecked(self);
pinned_self.sampleRate()
}
}
pub fn channel_count(&self) -> i32 {
unsafe {
let pinned_self = Pin::new_unchecked(self);
pinned_self.channels()
}
}
}
impl bindings::OpusFile {
pub fn xiph_comments(&self) -> Option<&bindings::XiphComment> {
let tag = unsafe {
// SAFETY: This will not exist beyond the scope of this function,
// and will only be used over ffi as a c++ this pointer (which is
// also pinned)
let pinned_self = Pin::new_unchecked(self);
pinned_self.opusTag()
};
unsafe {
// SAFETY: This pointer is a valid type, and can only used and accessed
// via this function and thus cannot be mutated, satisfying the aliasing rules.
tag.as_ref()
}
}
}
impl bindings::VorbisFile {
pub fn xiph_comments(&self) -> Option<&bindings::XiphComment> {
let tag = unsafe {
// SAFETY: This will not exist beyond the scope of this function,
// and will only be used over ffi as a c++ this pointer (which is
// also pinned)
let pinned_self = Pin::new_unchecked(self);
pinned_self.tag()
};
unsafe {
// SAFETY: This pointer is a valid type, and can only used and accessed
// via this function and thus cannot be mutated, satisfying the aliasing rules.
tag.as_ref()
}
}
}
impl bindings::XiphComment {
pub fn field_list_map(&self) -> &bindings::SimplePropertyMap {
unsafe {
// SAFETY: This will not exist beyond the scope of this function,
// and will only be used over ffi as a c++ this pointer (which is
// also pinned)
let pinned_self = Pin::new_unchecked(self);
pinned_self.fieldListMap()
}
}
}
impl bindings::SimplePropertyMap {
pub fn to_hashmap(&self) -> HashMap<String, Vec<String>> {
let cxx_vec = unsafe {
// SAFETY: This will not exist beyond the scope of this function,
// and will only be used over ffi as a c++ this pointer (which is
// also pinned)
let pinned_self = Pin::new_unchecked(self);
bindings::SimplePropertyMap_to_vector(pinned_self)
};
cxx_vec.iter().map(|property| property.to_tuple()).collect()
}
}
impl bindings::Property {
pub fn to_tuple(&self) -> (String, Vec<String>) {
unsafe {
// SAFETY: This will not exist beyond the scope of this function,
// and will only be used over ffi as a c++ this pointer (which is
// also pinned)
let pinned_self = Pin::new_unchecked(self);
let key = pinned_self.key().to_string();
let value = pinned_self.value().to_vec();
(key, value)
}
}
}
impl ToString for bindings::TagString {
fn to_string(&self) -> String {
let c_str = unsafe {
// SAFETY: This will not exist beyond the scope of this function,
// and will only be used over ffi as a c++ this pointer (which is
// also pinned)
let this = Pin::new_unchecked(self);
this.toCString(true)
};
unsafe {
// SAFETY: This is an output from C++ with a null pointer
// by design. It will not be mutated and is instantly copied
// into rust.
CStr::from_ptr(c_str)
}
.to_string_lossy()
.to_string()
}
}
impl bindings::StringList {
pub fn to_vec(&self) -> Vec<String> {
let cxx_values = unsafe {
// SAFETY: This will not exist beyond the scope of this function,
// and will only be used over ffi as a c++ this pointer (which is
// also pinned)
let pinned_self = Pin::new_unchecked(self);
bindings::StringList_to_vector(pinned_self)
};
cxx_values.iter().map(|value| value.to_string()).collect()
} }
} }

View file

@ -2,8 +2,6 @@ mod ffi;
mod stream; mod stream;
use ffi::bindings; use ffi::bindings;
use std::ffi::CStr;
use std::pin::{pin, Pin};
use std::collections::HashMap; use std::collections::HashMap;
pub use stream::{RustStream, TagLibStream}; pub use stream::{RustStream, TagLibStream};
@ -53,7 +51,7 @@ pub struct AudioProperties {
// Safe wrapper for FileRef that owns extracted data // Safe wrapper for FileRef that owns extracted data
pub struct FileRef { pub struct FileRef {
file: File, file: Option<File>,
} }
impl FileRef { impl FileRef {
@ -75,127 +73,36 @@ impl FileRef {
} }
// Extract data from C++ objects // Extract data from C++ objects
let pinned_file_ref = unsafe { Pin::new_unchecked(file_ref.as_ref().unwrap()) }; let file = file_ref.file_or().and_then(|file| {
let file_ptr = pinned_file_ref.file(); let audio_properties = file.audio_properties().map(|props| AudioProperties {
length_in_milliseconds: props.length_ms(),
bitrate_in_kilobits_per_second: props.bitrate_kbps(),
sample_rate_in_hz: props.sample_rate_hz(),
number_of_channels: props.channel_count(),
});
// Extract audio properties if let Some(vorbis_file) = file.as_vorbis() {
let audio_properties = { let xiph_comments = vorbis_file
let pinned_file = unsafe { Pin::new_unchecked(&*file_ptr) }; .xiph_comments()
let props_ptr = pinned_file.audioProperties(); .map(|comments| comments.field_list_map().to_hashmap());
if !props_ptr.is_null() {
let props = unsafe { Pin::new_unchecked(&*props_ptr) }; Some(File::OGG {
Some(AudioProperties { audio_properties,
length_in_milliseconds: props.lengthInMilliseconds(), xiph_comments,
bitrate_in_kilobits_per_second: props.bitrate(), })
sample_rate_in_hz: props.sampleRate(), } else if let Some(opus_file) = file.as_opus() {
number_of_channels: props.channels(), let xiph_comments = opus_file
.xiph_comments()
.map(|comments| comments.field_list_map().to_hashmap());
Some(File::Opus {
audio_properties,
xiph_comments,
}) })
} else { } else {
None Some(File::Unknown { audio_properties })
} }
}; });
// Determine file type and create appropriate variant
let file = unsafe {
let mpeg_file = ffi::bindings::File_asMPEG(file_ptr);
if !mpeg_file.is_null() {
return Some(FileRef {
file: File::MP3 { audio_properties }
});
}
let flac_file = ffi::bindings::File_asFLAC(file_ptr);
if !flac_file.is_null() {
return Some(FileRef {
file: File::FLAC { audio_properties, xiph_comments: None }
});
}
let mp4_file = ffi::bindings::File_asMP4(file_ptr);
if !mp4_file.is_null() {
return Some(FileRef {
file: File::MP4 { audio_properties }
});
}
let wav_file = ffi::bindings::File_asWAV(file_ptr);
if !wav_file.is_null() {
return Some(FileRef {
file: File::WAV { audio_properties }
});
}
let wavpack_file = ffi::bindings::File_asWavPack(file_ptr);
if !wavpack_file.is_null() {
return Some(FileRef {
file: File::WavPack { audio_properties }
});
}
let ape_file = ffi::bindings::File_asAPE(file_ptr);
if !ape_file.is_null() {
return Some(FileRef {
file: File::APE { audio_properties }
});
}
let vorbis_file = ffi::bindings::File_asVorbis(file_ptr);
if !vorbis_file.is_null() {
let pinned_vorbis_file = Pin::new_unchecked(&*vorbis_file);
let xiph_comments = pinned_vorbis_file.tag();
let pinned_xiph_comments = Pin::new_unchecked(&*xiph_comments);
let xiph_map = pinned_xiph_comments.fieldListMap();
let pinned_xiph_map = Pin::new_unchecked(xiph_map);
let xiph_comments = ffi::bindings::SimplePropertyMap_to_vector(pinned_xiph_map);
let mut xiph_safe_map = XiphComments::new();
for property in xiph_comments.iter() {
let pinned_property = Pin::new_unchecked(property);
let tag_key = pinned_property.key();
let pinned_key = Pin::new_unchecked(&*tag_key);
let c_str = pinned_key.toCString(true);
let key = CStr::from_ptr(c_str).to_string_lossy().to_string();
let tag_values = pinned_property.value();
let pinned_values = Pin::new_unchecked(&*tag_values);
let cxx_vec_values = ffi::bindings::StringList_to_vector(pinned_values);
let values = cxx_vec_values.iter().map(|value| {
let pinned_value = Pin::new_unchecked(value);
let c_str = pinned_value.toCString(true);
CStr::from_ptr(c_str).to_string_lossy().to_string()
}).collect();
xiph_safe_map.insert(key, values);
}
return Some(FileRef {
file: File::OGG { audio_properties, xiph_comments: Some(xiph_safe_map) }
});
}
let opus_file = ffi::bindings::File_asOpus(file_ptr);
if !opus_file.is_null() {
let pinned_opus_file = Pin::new_unchecked(&*opus_file);
let xiph_comments = pinned_opus_file.tag();
let pinned_xiph_comments = Pin::new_unchecked(&*xiph_comments);
let xiph_map = pinned_xiph_comments.fieldListMap();
let pinned_xiph_map = Pin::new_unchecked(xiph_map);
let xiph_comments = ffi::bindings::SimplePropertyMap_to_vector(pinned_xiph_map);
let mut xiph_safe_map = XiphComments::new();
for property in xiph_comments.iter() {
let pinned_property = Pin::new_unchecked(property);
let tag_key = pinned_property.key();
let pinned_key = Pin::new_unchecked(&*tag_key);
}
return Some(FileRef {
file: File::Opus { audio_properties, xiph_comments: Some(xiph_safe_map) }
});
}
File::Unknown { audio_properties }
};
// Clean up C++ objects - they will be dropped when file_ref is dropped // Clean up C++ objects - they will be dropped when file_ref is dropped
drop(file_ref); drop(file_ref);
@ -203,7 +110,7 @@ impl FileRef {
Some(FileRef { file }) Some(FileRef { file })
} }
pub fn file(&self) -> &File { pub fn file(&self) -> &Option<File> {
&self.file &self.file
} }
} }