Skip to content

Commit c2bc42f

Browse files
committed
Address bot reviews
1 parent 67b4c22 commit c2bc42f

18 files changed

Lines changed: 294 additions & 120 deletions

File tree

sdk/runanywhere-commons/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,11 @@ set(_SAVED_BUILD_SHARED_LIBS ${BUILD_SHARED_LIBS})
198198
set(BUILD_SHARED_LIBS OFF)
199199
FetchContent_MakeAvailable(libarchive)
200200
set(BUILD_SHARED_LIBS ${_SAVED_BUILD_SHARED_LIBS})
201+
202+
if(TARGET bz2_bundled AND TARGET archive_static)
203+
target_link_libraries(archive_static bz2_bundled)
204+
endif()
205+
201206
message(STATUS "libarchive ready (v${LIBARCHIVE_VERSION})")
202207

203208
# =============================================================================

sdk/runanywhere-commons/include/rac/core/capabilities/rac_lifecycle.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,25 @@ RAC_API rac_handle_t rac_lifecycle_get_service(rac_handle_t handle);
234234
*/
235235
RAC_API rac_result_t rac_lifecycle_require_service(rac_handle_t handle, rac_handle_t* out_service);
236236

237+
/**
238+
* @brief Acquire (pin) the current service, preventing unload while held.
239+
*
240+
* Increments an internal refcount. The caller MUST call rac_lifecycle_release_service()
241+
* when done. Unload/destroy will block until all acquired references are released.
242+
*
243+
* @param handle Lifecycle manager handle
244+
* @param out_service Output: Service handle (pinned)
245+
* @return RAC_SUCCESS or RAC_ERROR_NOT_INITIALIZED if not loaded
246+
*/
247+
RAC_API rac_result_t rac_lifecycle_acquire_service(rac_handle_t handle, rac_handle_t* out_service);
248+
249+
/**
250+
* @brief Release a previously acquired service reference.
251+
*
252+
* @param handle Lifecycle manager handle
253+
*/
254+
RAC_API void rac_lifecycle_release_service(rac_handle_t handle);
255+
237256
/**
238257
* @brief Track an operation error
239258
*

sdk/runanywhere-commons/include/rac/infrastructure/extraction/rac_extraction.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,19 @@ typedef struct rac_extraction_options {
4848
/**
4949
* @brief Default extraction options.
5050
*/
51+
#ifdef __cplusplus
52+
inline constexpr rac_extraction_options_t RAC_EXTRACTION_OPTIONS_DEFAULT = {
53+
RAC_TRUE, /* skip_macos_resources */
54+
RAC_FALSE, /* skip_symlinks */
55+
RAC_ARCHIVE_TYPE_NONE /* archive_type_hint */
56+
};
57+
#else
5158
static const rac_extraction_options_t RAC_EXTRACTION_OPTIONS_DEFAULT = {
5259
RAC_TRUE, /* skip_macos_resources */
5360
RAC_FALSE, /* skip_symlinks */
5461
RAC_ARCHIVE_TYPE_NONE /* archive_type_hint */
5562
};
63+
#endif
5664

5765
// =============================================================================
5866
// EXTRACTION RESULT

sdk/runanywhere-commons/src/backends/onnx/onnx_backend.cpp

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,8 @@
2424
#include "rac/core/rac_logger.h"
2525

2626
#if SHERPA_ONNX_AVAILABLE
27-
extern "C" {
28-
int espeak_Initialize(int output, int buflength, const char *path, int options);
29-
int espeak_SetVoiceByName(const char *name);
30-
}
31-
#define ESPEAK_AUDIO_OUTPUT_SYNCHRONOUS 0x0003
27+
// espeak-ng reinitialization is handled by destroying and recreating
28+
// the SherpaOnnxOfflineTts instance via the sherpa-onnx C API.
3229
#endif
3330

3431
namespace runanywhere {
@@ -966,26 +963,15 @@ bool ONNXTTS::load_model(const std::string& model_path, TTSModelType model_type,
966963
return false;
967964
}
968965

969-
sherpa_tts_ = new_tts;
970-
971-
// Force espeak-ng to use THIS model's data_dir.
972-
// Sherpa-ONNX uses std::once_flag for espeak_Initialize, so only the first
973-
// model loaded gets its data_dir registered. Re-calling espeak_Initialize
974-
// directly resets the internal path_home to the current model's directory.
975-
if (!espeak_data_dir_.empty()) {
976-
int reinit = espeak_Initialize(ESPEAK_AUDIO_OUTPUT_SYNCHRONOUS, 0, espeak_data_dir_.c_str(), 0);
977-
RAC_LOG_INFO("ONNX.TTS", "espeak_Initialize override: result=%d (expected 22050), data_dir=%s",
978-
reinit, espeak_data_dir_.c_str());
979-
980-
if (reinit == 22050) {
981-
int voice_test = espeak_SetVoiceByName("en-us");
982-
RAC_LOG_INFO("ONNX.TTS", "espeak_SetVoiceByName('en-us') test: result=%d (0=success)", voice_test);
983-
int voice_test_gb = espeak_SetVoiceByName("en-gb");
984-
RAC_LOG_INFO("ONNX.TTS", "espeak_SetVoiceByName('en-gb') test: result=%d (0=success)", voice_test_gb);
985-
} else {
986-
RAC_LOG_ERROR("ONNX.TTS", "espeak_Initialize override FAILED with code %d", reinit);
987-
}
966+
// Workaround for sherpa-onnx std::once_flag issue: espeak_Initialize is
967+
// only called internally on the very first SherpaOnnxCreateOfflineTts call.
968+
// When switching TTS models with different data_dir, destroy and recreate
969+
// the instance so the config (including data_dir) is applied correctly.
970+
if (sherpa_tts_ && sherpa_tts_ != new_tts) {
971+
SherpaOnnxDestroyOfflineTts(sherpa_tts_);
972+
sherpa_tts_ = nullptr;
988973
}
974+
sherpa_tts_ = new_tts;
989975

990976
sample_rate_ = SherpaOnnxOfflineTtsSampleRate(sherpa_tts_);
991977
int num_speakers = SherpaOnnxOfflineTtsNumSpeakers(sherpa_tts_);

sdk/runanywhere-commons/src/core/capabilities/lifecycle_manager.cpp

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ struct LifecycleManager {
6060
// Thread safety
6161
std::mutex mutex{};
6262

63+
// Service pinning (acquire/release) — prevents unload while service is in use
64+
std::atomic<int> service_refcount{0};
65+
std::condition_variable service_cv{};
66+
6367
LifecycleManager() {
6468
// Set start time (mirrors Swift's startTime = Date())
6569
auto now = std::chrono::system_clock::now();
@@ -225,13 +229,45 @@ rac_result_t rac_lifecycle_load(rac_handle_t handle, const char* model_path, con
225229
return result;
226230
}
227231

232+
rac_result_t rac_lifecycle_acquire_service(rac_handle_t handle, rac_handle_t* out_service) {
233+
if (handle == nullptr || out_service == nullptr) {
234+
return RAC_ERROR_NULL_POINTER;
235+
}
236+
237+
auto* mgr = static_cast<LifecycleManager*>(handle);
238+
std::lock_guard<std::mutex> lock(mgr->mutex);
239+
240+
if (mgr->state.load() != RAC_LIFECYCLE_STATE_LOADED || mgr->current_service.load() == nullptr) {
241+
return RAC_ERROR_NOT_INITIALIZED;
242+
}
243+
244+
mgr->service_refcount.fetch_add(1);
245+
*out_service = mgr->current_service.load();
246+
return RAC_SUCCESS;
247+
}
248+
249+
void rac_lifecycle_release_service(rac_handle_t handle) {
250+
if (handle == nullptr) {
251+
return;
252+
}
253+
254+
auto* mgr = static_cast<LifecycleManager*>(handle);
255+
int prev = mgr->service_refcount.fetch_sub(1);
256+
if (prev <= 1) {
257+
mgr->service_cv.notify_all();
258+
}
259+
}
260+
228261
rac_result_t rac_lifecycle_unload(rac_handle_t handle) {
229262
if (handle == nullptr) {
230263
return RAC_ERROR_NULL_POINTER;
231264
}
232265

233266
auto* mgr = static_cast<LifecycleManager*>(handle);
234-
std::lock_guard<std::mutex> lock(mgr->mutex);
267+
std::unique_lock<std::mutex> lock(mgr->mutex);
268+
269+
// Wait for all acquired services to be released
270+
mgr->service_cv.wait(lock, [mgr] { return mgr->service_refcount.load() == 0; });
235271

236272
// Mirrors Swift: if let modelId = await lifecycle.currentResourceId
237273
if (!mgr->current_model_id.empty()) {
@@ -269,7 +305,10 @@ rac_result_t rac_lifecycle_reset(rac_handle_t handle) {
269305
}
270306

271307
auto* mgr = static_cast<LifecycleManager*>(handle);
272-
std::lock_guard<std::mutex> lock(mgr->mutex);
308+
std::unique_lock<std::mutex> lock(mgr->mutex);
309+
310+
// Wait for all acquired services to be released
311+
mgr->service_cv.wait(lock, [mgr] { return mgr->service_refcount.load() == 0; });
273312

274313
// Track unload if currently loaded (mirrors Swift reset())
275314
if (!mgr->current_model_id.empty()) {

sdk/runanywhere-commons/src/features/diffusion/diffusion_component.cpp

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -396,8 +396,8 @@ extern "C" rac_result_t rac_diffusion_component_generate(rac_handle_t handle,
396396
// Reset cancellation flag (also atomic, but set under lock for consistency)
397397
component->cancel_requested = false;
398398

399-
// Get service from lifecycle manager
400-
rac_result_t result = rac_lifecycle_require_service(component->lifecycle, &service);
399+
// Pin service via acquire to prevent unload during generation
400+
rac_result_t result = rac_lifecycle_acquire_service(component->lifecycle, &service);
401401
if (result != RAC_SUCCESS) {
402402
RAC_LOG_ERROR("Diffusion.Component", "No model loaded - cannot generate");
403403
return result;
@@ -418,6 +418,9 @@ extern "C" rac_result_t rac_diffusion_component_generate(rac_handle_t handle,
418418
// Perform generation outside lock
419419
rac_result_t result = rac_diffusion_generate(service, &effective_options, out_result);
420420

421+
// Release pinned service in all exit paths
422+
rac_lifecycle_release_service(component->lifecycle);
423+
421424
if (result != RAC_SUCCESS) {
422425
RAC_LOG_ERROR("Diffusion.Component", "Generation failed: %d", result);
423426
rac_lifecycle_track_error(component->lifecycle, result, "generate");
@@ -491,8 +494,8 @@ extern "C" rac_result_t rac_diffusion_component_generate_with_callbacks(
491494
// Reset cancellation flag
492495
component->cancel_requested = false;
493496

494-
// Get service from lifecycle manager
495-
rac_result_t result = rac_lifecycle_require_service(component->lifecycle, &service);
497+
// Pin service via acquire to prevent unload during generation
498+
rac_result_t result = rac_lifecycle_acquire_service(component->lifecycle, &service);
496499
if (result != RAC_SUCCESS) {
497500
RAC_LOG_ERROR("Diffusion.Component", "No model loaded - cannot generate");
498501
if (error_callback) {
@@ -526,6 +529,9 @@ extern "C" rac_result_t rac_diffusion_component_generate_with_callbacks(
526529
rac_result_t result = rac_diffusion_generate_with_progress(service, &effective_options,
527530
diffusion_progress_wrapper, &ctx, &gen_result);
528531

532+
// Release pinned service in all exit paths
533+
rac_lifecycle_release_service(component->lifecycle);
534+
529535
if (result != RAC_SUCCESS) {
530536
RAC_LOG_ERROR("Diffusion.Component", "Generation failed: %d", result);
531537
rac_lifecycle_track_error(component->lifecycle, result, "generateWithCallbacks");

sdk/runanywhere-commons/src/features/llm/llm_component.cpp

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -223,9 +223,14 @@ extern "C" void rac_llm_component_destroy(rac_handle_t handle) {
223223

224224
auto* component = reinterpret_cast<rac_llm_component*>(handle);
225225

226-
// Destroy lifecycle manager (will cleanup service if loaded)
227-
if (component->lifecycle) {
228-
rac_lifecycle_destroy(component->lifecycle);
226+
// Acquire component mutex to serialize against in-flight operations.
227+
// lifecycle_destroy -> unload will block until any acquired services are released.
228+
{
229+
std::lock_guard<std::mutex> lock(component->mtx);
230+
if (component->lifecycle) {
231+
rac_lifecycle_destroy(component->lifecycle);
232+
component->lifecycle = nullptr;
233+
}
229234
}
230235

231236
log_info("LLM.Component", "LLM component destroyed");
@@ -771,13 +776,14 @@ extern "C" rac_result_t rac_llm_component_cancel(rac_handle_t handle) {
771776

772777
auto* component = reinterpret_cast<rac_llm_component*>(handle);
773778

774-
// Do NOT acquire component->mtx here. generate_stream() holds the mutex
775-
// for the entire streaming duration, so locking here would deadlock until
776-
// generation finishes — defeating the purpose of cancel.
777-
// rac_llm_cancel() ultimately sets an atomic bool, which is safe without the lock.
778-
rac_handle_t service = rac_lifecycle_get_service(component->lifecycle);
779-
if (service) {
779+
// Use acquire/release to pin the service for the duration of the cancel call,
780+
// preventing use-after-free if destroy races with cancel.
781+
// Do NOT acquire component->mtx — generate_stream() holds it during streaming.
782+
rac_handle_t service = nullptr;
783+
rac_result_t acq = rac_lifecycle_acquire_service(component->lifecycle, &service);
784+
if (acq == RAC_SUCCESS && service) {
780785
rac_llm_cancel(service);
786+
rac_lifecycle_release_service(component->lifecycle);
781787
}
782788

783789
log_info("LLM.Component", "Generation cancellation requested");

sdk/runanywhere-commons/src/features/vad/vad_analytics.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,11 @@ rac_result_t rac_vad_analytics_create(rac_vad_analytics_handle_t* out_handle) {
7171
return RAC_ERROR_INVALID_PARAMETER;
7272
}
7373

74-
*out_handle = new (std::nothrow) rac_vad_analytics_s();
74+
try {
75+
*out_handle = new (std::nothrow) rac_vad_analytics_s();
76+
} catch (...) {
77+
*out_handle = nullptr;
78+
}
7579
if (!*out_handle) {
7680
return RAC_ERROR_OUT_OF_MEMORY;
7781
}

sdk/runanywhere-commons/src/features/vad/vad_component.cpp

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* Do NOT add features not present in the Swift code.
1010
*/
1111

12+
#include <atomic>
1213
#include <chrono>
1314
#include <cstdlib>
1415
#include <cstring>
@@ -42,8 +43,8 @@ struct rac_vad_component {
4243
rac_vad_audio_callback_fn audio_callback;
4344
void* audio_user_data;
4445

45-
/** Initialization state */
46-
bool is_initialized;
46+
/** Initialization state (atomic for lock-free query from callbacks) */
47+
std::atomic<bool> is_initialized;
4748

4849
/** Mutex for thread safety */
4950
std::mutex mtx;
@@ -181,8 +182,7 @@ extern "C" rac_bool_t rac_vad_component_is_initialized(rac_handle_t handle) {
181182
return RAC_FALSE;
182183

183184
auto* component = reinterpret_cast<rac_vad_component*>(handle);
184-
std::lock_guard<std::mutex> lock(component->mtx);
185-
return component->is_initialized ? RAC_TRUE : RAC_FALSE;
185+
return component->is_initialized.load(std::memory_order_acquire) ? RAC_TRUE : RAC_FALSE;
186186
}
187187

188188
extern "C" rac_result_t rac_vad_component_initialize(rac_handle_t handle) {
@@ -479,13 +479,8 @@ extern "C" rac_lifecycle_state_t rac_vad_component_get_state(rac_handle_t handle
479479
return RAC_LIFECYCLE_STATE_IDLE;
480480

481481
auto* component = reinterpret_cast<rac_vad_component*>(handle);
482-
std::lock_guard<std::mutex> lock(component->mtx);
483-
484-
if (component->is_initialized) {
485-
return RAC_LIFECYCLE_STATE_LOADED;
486-
}
487-
488-
return RAC_LIFECYCLE_STATE_IDLE;
482+
return component->is_initialized.load(std::memory_order_acquire) ? RAC_LIFECYCLE_STATE_LOADED
483+
: RAC_LIFECYCLE_STATE_IDLE;
489484
}
490485

491486
extern "C" rac_result_t rac_vad_component_get_metrics(rac_handle_t handle,

sdk/runanywhere-commons/src/features/voice_agent/voice_agent.cpp

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include <cstring>
1414
#include <mutex>
1515
#include <new>
16+
#include <thread>
1617

1718
#include "rac/core/rac_analytics_events.h"
1819
#include "rac/core/rac_audio_utils.h"
@@ -48,6 +49,10 @@ struct rac_voice_agent {
4849
// State — atomic so is_ready() checks don't need the mutex
4950
std::atomic<bool> is_configured{false};
5051

52+
// Shutdown barrier — prevents use-after-free on destroy
53+
std::atomic<bool> is_shutting_down{false};
54+
std::atomic<int> in_flight{0};
55+
5156
// Whether we own the component handles (and should destroy them)
5257
bool owns_components;
5358

@@ -237,12 +242,17 @@ void rac_voice_agent_destroy(rac_voice_agent_handle_t handle) {
237242
return;
238243
}
239244

245+
// Signal shutdown and wait for all in-flight operations (including lock-free ones)
246+
handle->is_shutting_down.store(true, std::memory_order_release);
247+
handle->is_configured.store(false, std::memory_order_release);
248+
249+
// Spin-wait until all in-flight operations complete
250+
while (handle->in_flight.load(std::memory_order_acquire) > 0) {
251+
std::this_thread::yield();
252+
}
253+
240254
{
241-
// Acquire mutex so any in-flight process_voice_turn / process_stream
242-
// finishes before we tear down components. Setting is_configured=false
243-
// ensures new callers return immediately after we release.
244255
std::lock_guard<std::mutex> lock(handle->mutex);
245-
handle->is_configured.store(false, std::memory_order_release);
246256

247257
if (handle->owns_components) {
248258
RAC_LOG_DEBUG("VoiceAgent", "Destroying owned component handles");
@@ -909,10 +919,23 @@ rac_result_t rac_voice_agent_detect_speech(rac_voice_agent_handle_t handle, cons
909919
return RAC_ERROR_INVALID_ARGUMENT;
910920
}
911921

922+
// Check shutdown barrier (this is a lock-free path)
923+
if (handle->is_shutting_down.load(std::memory_order_acquire)) {
924+
return RAC_ERROR_INVALID_STATE;
925+
}
926+
handle->in_flight.fetch_add(1, std::memory_order_acq_rel);
927+
928+
// Re-check after incrementing to avoid TOCTOU with destroy
929+
if (handle->is_shutting_down.load(std::memory_order_acquire)) {
930+
handle->in_flight.fetch_sub(1, std::memory_order_acq_rel);
931+
return RAC_ERROR_INVALID_STATE;
932+
}
933+
912934
// VAD doesn't require is_configured (mirrors Swift)
913935
rac_result_t result =
914936
rac_vad_component_process(handle->vad_handle, samples, sample_count, out_speech_detected);
915937

938+
handle->in_flight.fetch_sub(1, std::memory_order_acq_rel);
916939
return result;
917940
}
918941

0 commit comments

Comments
 (0)