@@ -20,9 +20,6 @@ import 'package:runanywhere/native/ffi_types.dart';
2020import 'package:runanywhere/native/native_functions.dart' ;
2121import 'package:runanywhere/native/platform_loader.dart' ;
2222
23- /// Voice agent handle type (opaque pointer to rac_voice_agent struct).
24- typedef RacVoiceAgentHandle = Pointer <Void >;
25-
2623/// VoiceAgent component bridge for C++ interop.
2724///
2825/// Orchestrates LLM, STT, TTS, and VAD components for voice conversations.
@@ -45,7 +42,8 @@ class DartBridgeVoiceAgent {
4542
4643 // MARK: - State
4744
48- RacVoiceAgentHandle ? _handle;
45+ RacHandle ? _handle;
46+ Future <RacHandle >? _initFuture;
4947 final _logger = SDKLogger ('DartBridge.VoiceAgent' );
5048
5149 /// Event stream controller
@@ -60,11 +58,20 @@ class DartBridgeVoiceAgent {
6058 ///
6159 /// Requires LLM, STT, TTS, and VAD components to be available.
6260 /// Uses shared component handles (matches Swift CppBridge+VoiceAgent.swift).
63- Future <RacVoiceAgentHandle > getHandle () async {
61+ Future <RacHandle > getHandle () async {
6462 if (_handle != null ) {
6563 return _handle! ;
6664 }
6765
66+ final initFuture = _initFuture;
67+ if (initFuture != null ) {
68+ await initFuture;
69+ return _handle! ;
70+ }
71+
72+ final completer = Completer <RacHandle >();
73+ _initFuture = completer.future;
74+
6875 // Use shared component handles (matches Swift approach)
6976 // This allows the voice agent to use already-loaded models from the
7077 // individual component bridges (STT, LLM, TTS, VAD)
@@ -77,7 +84,7 @@ class DartBridgeVoiceAgent {
7784 'Creating voice agent with shared handles: LLM=$llmHandle , STT=$sttHandle , TTS=$ttsHandle , VAD=$vadHandle ' );
7885
7986 try {
80- final handlePtr = calloc <RacVoiceAgentHandle >();
87+ final handlePtr = calloc <RacHandle >();
8188 try {
8289 final result = NativeFunctions .voiceAgentCreate (
8390 llmHandle, sttHandle, ttsHandle, vadHandle, handlePtr);
@@ -90,12 +97,18 @@ class DartBridgeVoiceAgent {
9097
9198 _handle = handlePtr.value;
9299 _logger.info ('Voice agent created with shared component handles' );
100+ completer.complete (_handle! );
101+ _initFuture = null ;
93102 return _handle! ;
94103 } finally {
95104 calloc.free (handlePtr);
96105 }
97- } catch (e) {
106+ } catch (e, st ) {
98107 _logger.error ('Failed to create voice agent handle: $e ' );
108+ if (! completer.isCompleted) {
109+ completer.completeError (e, st);
110+ }
111+ _initFuture = null ;
99112 rethrow ;
100113 }
101114 }
@@ -300,7 +313,7 @@ class DartBridgeVoiceAgent {
300313 /// Static helper for processing voice turn in an isolate.
301314 /// The C++ API expects raw audio bytes (PCM16), not float samples.
302315 static Future <VoiceTurnResult > _processVoiceTurnInIsolate (
303- RacVoiceAgentHandle handle,
316+ RacHandle handle,
304317 Uint8List audioData,
305318 ) async {
306319 // Allocate native memory for audio data (raw PCM16 bytes)
@@ -311,16 +324,8 @@ class DartBridgeVoiceAgent {
311324 // Efficient bulk copy of audio bytes
312325 audioPtr.asTypedList (audioData.length).setAll (0 , audioData);
313326
314- final lib = PlatformLoader .loadCommons ();
315- final processFn = lib.lookupFunction<
316- Int32 Function (RacVoiceAgentHandle , Pointer <Void >, IntPtr ,
317- Pointer <RacVoiceAgentResultStruct >),
318- int Function (RacVoiceAgentHandle , Pointer <Void >, int ,
319- Pointer <RacVoiceAgentResultStruct >)> (
320- 'rac_voice_agent_process_voice_turn' );
321-
322- final status =
323- processFn (handle, audioPtr.cast <Void >(), audioData.length, resultPtr);
327+ final status = _processVoiceTurnFn (
328+ handle, audioPtr.cast <Void >(), audioData.length, resultPtr);
324329
325330 if (status != RAC_SUCCESS ) {
326331 throw StateError (
@@ -329,20 +334,14 @@ class DartBridgeVoiceAgent {
329334 }
330335
331336 // Parse result while still in isolate (before freeing memory)
332- return _parseVoiceTurnResultStatic (resultPtr.ref, lib );
337+ return _parseVoiceTurnResultStatic (resultPtr.ref, _voiceAgentLib );
333338 } finally {
334339 // Free audio data
335340 calloc.free (audioPtr);
336341
337342 // Free result struct - the C++ side allocates strings/audio that need freeing
338- final lib = PlatformLoader .loadCommons ();
339343 try {
340- final freeFn = lib.lookupFunction<
341- Void Function (Pointer <RacVoiceAgentResultStruct >),
342- void Function (Pointer <RacVoiceAgentResultStruct >)> (
343- 'rac_voice_agent_result_free' ,
344- );
345- freeFn (resultPtr);
344+ _voiceAgentResultFreeFn? .call (resultPtr);
346345 } catch (e) {
347346 // Function may not exist, just free the struct
348347 }
@@ -600,3 +599,34 @@ final class RacVoiceAgentResultStruct extends Struct {
600599 @IntPtr ()
601600 external int synthesizedAudioSize; // size_t (size in bytes)
602601}
602+
603+ // MARK: - Isolate-scoped FFI caches
604+
605+ // These are intentionally top-level statics so each isolate initializes them
606+ // once on first use. This keeps symbol lookups out of hot paths while preserving
607+ // the existing isolate execution model.
608+ final DynamicLibrary _voiceAgentLib = PlatformLoader .loadCommons ();
609+
610+ final int Function (
611+ RacHandle ,
612+ Pointer <Void >,
613+ int ,
614+ Pointer <RacVoiceAgentResultStruct >,
615+ ) _processVoiceTurnFn = _voiceAgentLib.lookupFunction<
616+ Int32 Function (RacHandle , Pointer <Void >, IntPtr ,
617+ Pointer <RacVoiceAgentResultStruct >),
618+ int Function (RacHandle , Pointer <Void >, int ,
619+ Pointer <RacVoiceAgentResultStruct >)> ('rac_voice_agent_process_voice_turn' );
620+
621+ final void Function (Pointer <RacVoiceAgentResultStruct >)?
622+ _voiceAgentResultFreeFn = (() {
623+ try {
624+ return _voiceAgentLib.lookupFunction<
625+ Void Function (Pointer <RacVoiceAgentResultStruct >),
626+ void Function (Pointer <RacVoiceAgentResultStruct >)> (
627+ 'rac_voice_agent_result_free' ,
628+ );
629+ } catch (_) {
630+ return null ;
631+ }
632+ })();
0 commit comments