using System.Runtime.InteropServices; // [UnmanagedCallersOnly] static methods for vc_callbacks.on_event/on_level โ€” true native // function pointers, not GC-tracked delegates (avoids the classic P/Invoke pitfall where a // delegate is collected by the GC sometime after the call that registered it returns; see // docs/tech-stack.md ยง2). vc_callbacks.user is a GCHandle-wrapped VoiceCatClient (allocated in // VoiceCatClient's constructor, freed in Dispose) โ€” these methods must be static, so they // resolve back to the right client instance via that handle rather than closing over state. namespace VoiceCat.Interop; internal static unsafe class NativeCallbacks { [UnmanagedCallersOnly] internal static void OnEvent(IntPtr userContext, VcEventNative* ev) { if (userContext == IntPtr.Zero) return; if (GCHandle.FromIntPtr(userContext).Target is not VoiceCatClient client) return; // CRITICAL (voicecat.h's vc_event doc comment): ev->Text is owned by the core and // valid ONLY for the duration of this callback. Convert to a managed string NOW, // before returning โ€” never store/queue the raw VcEventNative across the callback // boundary, or Text will be a dangling pointer by the time it's read. client.EnqueueEvent(VoiceCatEvent.FromNative(*ev)); } [UnmanagedCallersOnly] internal static void OnLevel(IntPtr userContext, uint streamId, float rms) { if (userContext == IntPtr.Zero) return; if (GCHandle.FromIntPtr(userContext).Target is not VoiceCatClient client) return; client.EnqueueLevel(streamId, rms); } }