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 {
// File conversion functions
TagLib::Ogg::Vorbis::File* File_asVorbis(TagLib::File* file) {
return dynamic_cast<TagLib::Ogg::Vorbis::File*>(file);
const TagLib::Ogg::File* File_asOgg(const TagLib::File* file) {
return dynamic_cast<const TagLib::Ogg::File*>(file);
}
TagLib::Ogg::Opus::File* File_asOpus(TagLib::File* file) {
return dynamic_cast<TagLib::Ogg::Opus::File*>(file);
const TagLib::Ogg::Vorbis::File* File_asVorbis(const TagLib::File* file) {
return dynamic_cast<const TagLib::Ogg::Vorbis::File*>(file);
}
TagLib::MPEG::File* File_asMPEG(TagLib::File* file) {
return dynamic_cast<TagLib::MPEG::File*>(file);
const TagLib::Ogg::Opus::File* File_asOpus(const TagLib::File* file) {
return dynamic_cast<const TagLib::Ogg::Opus::File*>(file);
}
TagLib::FLAC::File* File_asFLAC(TagLib::File* file) {
return dynamic_cast<TagLib::FLAC::File*>(file);
const TagLib::MPEG::File* File_asMPEG(const TagLib::File* file) {
return dynamic_cast<const TagLib::MPEG::File*>(file);
}
TagLib::MP4::File* File_asMP4(TagLib::File* file) {
return dynamic_cast<TagLib::MP4::File*>(file);
const TagLib::FLAC::File* File_asFLAC(const TagLib::File* file) {
return dynamic_cast<const TagLib::FLAC::File*>(file);
}
TagLib::RIFF::WAV::File* File_asWAV(TagLib::File* file) {
return dynamic_cast<TagLib::RIFF::WAV::File*>(file);
const TagLib::MP4::File* File_asMP4(const TagLib::File* file) {
return dynamic_cast<const TagLib::MP4::File*>(file);
}
TagLib::WavPack::File* File_asWavPack(TagLib::File* file) {
return dynamic_cast<TagLib::WavPack::File*>(file);
const TagLib::RIFF::WAV::File* File_asWAV(const TagLib::File* file) {
return dynamic_cast<const TagLib::RIFF::WAV::File*>(file);
}
TagLib::APE::File* File_asAPE(TagLib::File* file) {
return dynamic_cast<TagLib::APE::File*>(file);
const TagLib::WavPack::File* File_asWavPack(const TagLib::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

View file

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

View file

@ -1,3 +1,8 @@
use std::ffi::CStr;
use std::pin::Pin;
use std::string::ToString;
use std::collections::HashMap;
#[cxx::bridge]
pub(crate) mod bindings {
unsafe extern "C++" {
@ -13,7 +18,7 @@ pub(crate) mod bindings {
#[namespace = "TagLib"]
type FileRef;
fn isNull(self: Pin<&FileRef>) -> bool;
fn file(self: Pin<&FileRef>) -> *mut File;
fn file(self: Pin<&FileRef>) -> *mut BaseFile;
#[namespace = "taglib_shim"]
type RustIOStream;
@ -27,8 +32,9 @@ pub(crate) mod bindings {
fn new_FileRef_from_stream(stream: UniquePtr<RustIOStream>) -> UniquePtr<FileRef>;
#[namespace = "TagLib"]
type File;
fn audioProperties(self: Pin<&File>) -> *mut AudioProperties;
#[cxx_name = "File"]
type BaseFile;
fn audioProperties(self: Pin<&BaseFile>) -> *mut AudioProperties;
#[namespace = "TagLib"]
type AudioProperties;
@ -37,29 +43,33 @@ pub(crate) mod bindings {
fn sampleRate(self: Pin<&AudioProperties>) -> i32;
fn channels(self: Pin<&AudioProperties>) -> i32;
#[namespace = "TagLib::Ogg::Vorbis"]
#[namespace = "TagLib::Ogg"]
#[cxx_name = "File"]
type VorbisFile;
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;
type OggFile;
#[namespace = "TagLib::Ogg"]
type XiphComment;
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"]
#[cxx_name = "File"]
type MPEGFile;
#[namespace = "TagLib::MP4"]
#[cxx_name = "File"]
type MP4File;
@ -78,26 +88,30 @@ pub(crate) mod bindings {
// File conversion functions
#[namespace = "taglib_shim"]
unsafe fn File_asVorbis(file: *mut File) -> *mut VorbisFile;
unsafe fn File_asOgg(file: *const BaseFile) -> *const OggFile;
#[namespace = "taglib_shim"]
unsafe fn File_asOpus(file: *mut File) -> *mut OpusFile;
unsafe fn File_asVorbis(file: *const BaseFile) -> *const VorbisFile;
#[namespace = "taglib_shim"]
unsafe fn File_asMPEG(file: *mut File) -> *mut MPEGFile;
unsafe fn File_asOpus(file: *const BaseFile) -> *const OpusFile;
#[namespace = "taglib_shim"]
unsafe fn File_asFLAC(file: *mut File) -> *mut FLACFile;
unsafe fn File_asMPEG(file: *const BaseFile) -> *const MPEGFile;
#[namespace = "taglib_shim"]
unsafe fn File_asMP4(file: *mut File) -> *mut MP4File;
unsafe fn File_asFLAC(file: *const BaseFile) -> *const FLACFile;
#[namespace = "taglib_shim"]
unsafe fn File_asWAV(file: *mut File) -> *mut WAVFile;
unsafe fn File_asMP4(file: *const BaseFile) -> *const MP4File;
#[namespace = "taglib_shim"]
unsafe fn File_asWavPack(file: *mut File) -> *mut WavPackFile;
unsafe fn File_asWAV(file: *const BaseFile) -> *const WAVFile;
#[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"]
type SimplePropertyMap;
#[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"]
type Property;
@ -114,7 +128,186 @@ pub(crate) mod bindings {
type TagString;
#[namespace = "taglib_shim"]
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;
use ffi::bindings;
use std::ffi::CStr;
use std::pin::{pin, Pin};
use std::collections::HashMap;
pub use stream::{RustStream, TagLibStream};
@ -53,7 +51,7 @@ pub struct AudioProperties {
// Safe wrapper for FileRef that owns extracted data
pub struct FileRef {
file: File,
file: Option<File>,
}
impl FileRef {
@ -75,127 +73,36 @@ impl FileRef {
}
// Extract data from C++ objects
let pinned_file_ref = unsafe { Pin::new_unchecked(file_ref.as_ref().unwrap()) };
let file_ptr = pinned_file_ref.file();
let file = file_ref.file_or().and_then(|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
let audio_properties = {
let pinned_file = unsafe { Pin::new_unchecked(&*file_ptr) };
let props_ptr = pinned_file.audioProperties();
if !props_ptr.is_null() {
let props = unsafe { Pin::new_unchecked(&*props_ptr) };
Some(AudioProperties {
length_in_milliseconds: props.lengthInMilliseconds(),
bitrate_in_kilobits_per_second: props.bitrate(),
sample_rate_in_hz: props.sampleRate(),
number_of_channels: props.channels(),
if let Some(vorbis_file) = file.as_vorbis() {
let xiph_comments = vorbis_file
.xiph_comments()
.map(|comments| comments.field_list_map().to_hashmap());
Some(File::OGG {
audio_properties,
xiph_comments,
})
} else if let Some(opus_file) = file.as_opus() {
let xiph_comments = opus_file
.xiph_comments()
.map(|comments| comments.field_list_map().to_hashmap());
Some(File::Opus {
audio_properties,
xiph_comments,
})
} 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
drop(file_ref);
@ -203,7 +110,7 @@ impl FileRef {
Some(FileRef { file })
}
pub fn file(&self) -> &File {
pub fn file(&self) -> &Option<File> {
&self.file
}
}