Skip to content

Owned callbacks for non-COM interfaces (XAudio2, SourceVoice) #4668

Description

@dotnetasyncawait

Suggestion

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

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:

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:

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    questionFurther information is requested

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions