microsoft / windows-rs

Rust for Windows
https://kennykerr.ca/rust-getting-started/
Apache License 2.0
10.33k stars 485 forks source link

Simplify non-COM interface parameter bindings #2098

Open stuntman11 opened 1 year ago

stuntman11 commented 1 year ago

Hello, the last couple of days I tried to migrate my XAudio2 project from C++ to Rust and was simply unable to implement the IXAudio2VoiceCallback interface.

I am quite new to the rust language and even after hours of research do not understand why the implement macro doesn't work in my case. I don't think this is a general rust question because the macro is a specific functionality of the windows crate.


I want to call the function CreateSourceVoice on an XAudio2 instance:

audio.CreateSourceVoice(&mut p_source_voice, &format, 0, 2.0, &audio_callback.into(), Some(&music_voice_sends), None)

To create an implementation for the IXAudio2VoiceCallback interface I use the implement macro:

#[implement(IXAudio2VoiceCallback)]
struct MyCallbacks {}

#[allow(non_snake_case)]
impl IXAudio2VoiceCallback_Impl for MyCallbacks {
    fn OnVoiceProcessingPassStart(&self, bytesrequired:u32) {}
    fn OnVoiceProcessingPassEnd(&self) {}
    fn OnStreamEnd(&self) {}
    fn OnBufferStart(&self, pbuffercontext: *mut core::ffi::c_void) {}
    fn OnBufferEnd(&self, pbuffercontext: *mut core::ffi::c_void) {}
    fn OnLoopEnd(&self, pbuffercontext: *mut core::ffi::c_void) {}
    fn OnVoiceError(&self, pbuffercontext: *mut core::ffi::c_void, error: windows::core::HRESULT) {}
}

The compiler error for the implement macro is:

this associated function takes 1 generic argument but 3 generic arguments were supplied
expected 1 generic argumentrustc[E0107](https://doc.rust-lang.org/error-index.html#E0107)
impl.rs(646, 18): associated function defined here, with 1 generic parameter: `Impl`
main.rs(14, 1): remove these generic arguments
the trait bound `IXAudio2VoiceCallback: RuntimeName` is not satisfied
the following other types implement trait `RuntimeName`:
  IActivateAudioInterfaceAsyncOperation
  IActivateAudioInterfaceCompletionHandler
  IAudioAmbisonicsControl
  IAudioAutoGainControl
  IAudioBass
  IAudioCaptureClient
  IAudioChannelConfig
  IAudioClient
and 77 othersrustc[E0277](https://doc.rust-lang.org/error-index.html#E0277)
inspectable.rs(134, 52): required by a bound in `IInspectable_Vtbl::new`
no function or associated item named `matches` found for struct `IXAudio2VoiceCallback_Vtbl` in the current scope
function or associated item not found in `IXAudio2VoiceCallback_Vtbl`rustc[E0599](https://doc.rust-lang.org/error-index.html#E0599)

The issue "How to get COM object as "base" Interface without move" shows a similar situation with the IAudioEndpointVolumeCallback interface but in their case it just works. It would be awesome if someone with more windows crate knowledge or more rust experience in generell could help me.

kennykerr commented 1 year ago

Hey, that's a confusing error! Getting Rust macros to produce meaningful errors is hard... Anyway, the trick is that the implement macro is only for COM interfaces and IXAudio2VoiceCallback is not a COM interface! A few Windows APIs rely on these Frankenstein interfaces that don't inherit from IUnknown and thus have no way to express lifetime and discovery.

The good news is that I recently provided support for such interfaces: #2066

stuntman11 commented 1 year ago

I applied the concepts from the example you provided in (#2066) to my XAudio2 problem and think it works. Now I use the interface macro to declare my own type trait for the IXAudio2VoiceCallback interface. But I have a follow-up question regarding the interface instantiation and to simplify the example I only show the OnBufferEnd method.

struct XAudio2VoiceCallbacks;

#[interface]
unsafe trait XAudio2VoiceCallbackInterface {
    unsafe fn OnBufferEnd(&self, pbuffercontext: *mut ::core::ffi::c_void);
}

impl XAudio2VoiceCallbackInterface_Impl for XAudio2VoiceCallbacks {
    unsafe fn OnStreamEnd(&self) {}
}

The final goal is to call the CreateSourceVoice() method which expects an Into<InParam<'a, IXAudio2VoiceCallback>> for the callbacks argument. At first I thought there must be some kind of constructor or conversion method to convert my callbacks struct to the official callbacks struct but didn't find anything. So do I use mem::transmute() to convert between the types?

let callback_instance = XAudio2VoiceCallbacks {};
let callback_interface = XAudio2VoiceCallbackInterface::new(&callback_instance);
let callback_windows = mem::transmute(callback_interface);

Thanks for your time! I am still in the process of learning the rust language and really appreciate the support.

kennykerr commented 1 year ago

No, since the IXAudio2VoiceCallback interface is defined by the windows crate, you need not use the interface macro to define it again. That macro exists only for those interfaces that aren't already defined by the windows crate.

So I don't know anything about the XAudio2 API but something like this should get you started:

[dependencies.windows]
version = "0.42"
features = [
    "implement",
    "Win32_Foundation",
    "Win32_Media_Audio_XAudio2",
    "Win32_System_Com",
    "Win32_System_SystemInformation",
]
use windows::{
    core::*, Win32::Media::Audio::XAudio2::*, Win32::System::Com::*,
    Win32::System::SystemInformation::*,
};

struct Callback;

impl IXAudio2VoiceCallback_Impl for Callback {
    fn OnVoiceProcessingPassStart(&self, _: u32) {
        todo!()
    }
    fn OnVoiceProcessingPassEnd(&self) {
        todo!()
    }
    fn OnStreamEnd(&self) {
        println!("callback");
    }
    fn OnBufferStart(&self, _: *mut std::ffi::c_void) {
        todo!()
    }
    fn OnBufferEnd(&self, _: *mut std::ffi::c_void) {
        todo!()
    }
    fn OnLoopEnd(&self, _: *mut std::ffi::c_void) {
        todo!()
    }
    fn OnVoiceError(&self, _: *mut std::ffi::c_void, _: HRESULT) {
        todo!()
    }
}

fn main() -> Result<()> {
    unsafe {
        CoInitializeEx(None, COINIT_MULTITHREADED)?;

        let mut audio = None;
        XAudio2CreateWithVersionInfo(&mut audio, 0, XAUDIO2_DEFAULT_PROCESSOR, NTDDI_VERSION)?;

        if let Some(audio) = audio {
            // Call the callback interface directly...
            let callback = IXAudio2VoiceCallback::new(&Callback);
            callback.OnStreamEnd();

           // Pass the callback to another API...
            let mut source = None;
            audio.CreateSourceVoice(
                &mut source,
                std::ptr::null(),
                0,
                0.0,
                &*callback,
                None,
                None,
            )?;
        }

        Ok(())
    }
}

Notice the weird &* that is required for complicated reasons. I hope to get rid of that soon.

Anyway, I hope that helps.

stuntman11 commented 1 year ago

Ohhhh wow the &* does the trick. Would never have guessed that. Thanks!

kennykerr commented 1 year ago

Great, I'll keep this open to remind me to get rid of the need for that trick.