#4668·windows-rs

`windows-bindgen` Owned callbacks for non-COM interfaces (XAudio2, SourceVoice)

Author: dotnetasyncawaitCreated Jun 30, 2026Updated Jul 7, 2026
Labelsquestion

Suggestion

Would it be possible to add <name>Callback::new functions that take ownership of a callback implementation, rather than borrowing it? Eg:

rust
impl IXAudio2VoiceCallback {
	// Now T is boxed and owned by this wrapper
	pub fn new_owned<T: IXAudio2VoiceCallback_Impl>(this: T) -> windows_core::NonScopedInterface<Self> { }
}

Specifically, IXAudio2SourceVoice callbacks MUST outlive the source voice. IXAudio2SourceVoice::Start is asynchronous, and returns before playback has finished. And the only way to figure out if the sound is finished is:

  • either by periodically fetching the state (IXAudio2SourceVoice::GetState)
  • or by getting a notification via IXAudio2VoiceCallback::OnBufferEnd callback, from which we would normally either destroy or reuse that voice.

And the following is UB:

rust
let callb = MyCallback;
let i_callb: ScopedInterface<'_, IXAudio2VoiceCallback> = IXAudio2VoiceCallback::new(&callb);
        
let mut source: Option<IXAudio2SourceVoice> = None;
ixaudio2.CreateSourceVoice(&mut source, pcallback: Some(i_callb.deref()), ...);

source.SubmitSourceBuffer(...)
source.Start(...)
return;

because we return while the source voice is still alive and expects the pointer to the vtable to be valid.

So the proper usage would be to instantiate the callback once at the very beginning, store and use it for each source voice creation:

rust
struct XAudio2 {
	ixaudio2: IXAudio2,
	callback: NonScopedInterface<IXAudio2VoiceCallback>,
}

impl XAudio2 {
	pub fn new() -> Self {
		// create IXAudio2, mastering voice, etc ...
		
		let callback = MyCallback;
		Self { ixaudio2, callback: IXAudio2VoiceCallback::new_owned(callback) }
	}
	
	pub fn play_sound(&self, path: ...) {
		let mut source: Option<IXAudio2SourceVoice> = None;
		self.ixaudio2.CreateSourceVoice(&mut source, ..., pcallback: Some(self.callback.deref()), ...);
		
		source.SubmitSourceBuffer(...);
		source.Start(...);
		return;
	}
}

With the current approach, storing both MyCallback and the ScopedInterface (which is tied to the callback's lifetime) inside a single struct would be a difficult challenge.