Skip to content

C++ layer full cleanup: correctness, ABI, backend abstraction, JNI safety#483

Open
sanchitmonga22 wants to merge 16 commits intomainfrom
smonga/cpp-layer-full-cleanup
Open

C++ layer full cleanup: correctness, ABI, backend abstraction, JNI safety#483
sanchitmonga22 wants to merge 16 commits intomainfrom
smonga/cpp-layer-full-cleanup

Conversation

@sanchitmonga22
Copy link
Copy Markdown
Contributor

@sanchitmonga22 sanchitmonga22 commented Apr 15, 2026

Summary

Full refactor of sdk/runanywhere-commons/ plus coordinated updates to all 5 SDK consumers (Swift, Kotlin, React Native, Flutter, Web). 14 commits, ~3,000 lines changed.

Driven by the 7-agent audit in CPP_LAYER_AUDIT.md at repo root. Every verified finding from that audit is either fixed here or listed as documented follow-up in sdk/runanywhere-commons/CPP_LAYER_CLEANUP_STATUS.md.

What this PR delivers

Correctness fixes

  • sherpa-onnx runtime ABI version check (was SIGSEGV on mismatch, now clean error)
  • JniScope RAII helper applied to all 17 native→Java callback sites in the JNI bridge (was 11/149 exception-check coverage, now 100%)
  • Completed the wakeword service wire-up to its existing ONNX backend (7 TODOs closed)
  • std::atomic on download manager health flags (was TSan data-race candidate)
  • macOS-host-JDK / Android-NDK AttachCurrentThread portability cast

Architectural foundations

  • RAC_NODISCARD on 479 public functions via new rac_attrs.h
  • rac_hardware_query_capabilities() public C API + rac_hardware_report_t versioned struct
  • Abstract rac::backends::IInferenceBackend class (incremental retrofit of 5 backends is documented follow-up)
  • New rac_wakeword_provider_ops_t vtable decouples the wakeword service from any specific backend

ABI evolution (breaking)

  • rac_platform_adapter_t.version field, enforced by rac_init
  • RAC_PLATFORM_ADAPTER_VERSION = 1 — any adapter with .version == 0 is rejected with RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION (no grace period)
  • New error codes: RAC_ERROR_BACKEND_UNAVAILABLE, RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION, RAC_ERROR_JNI_EXCEPTION

SDK consumer coordination

  • Swift: CppBridge+PlatformAdapter.swift sets version; CRACommons header fork synced
  • Kotlin/JNI: racSetPlatformAdapter sets g_c_adapter.version
  • React Native: InitBridge.cpp sets adapter_.version
  • Flutter: RacPlatformAdapterStruct has leading uint32 version; dart_bridge_platform.dart populates it
  • Web: PlatformAdapter.ts writes 4-byte version as first struct field

Tooling / hygiene

  • cmake/Sanitizers.cmake with ASan/UBSan/TSan/MSan options + mutual-exclusion guards
  • CMakePresets.json: dev-asan, dev-ubsan, dev-tsan presets
  • .clang-tidy tightened (nodiscard, owning-memory, slicing checks active)
  • rac_platform_compat.h moved from include/ to src/internal/ (resolves PR feat: support to build on windows #383 deferred TODO; stops leaking dirent/DIR/opendir/... into Windows consumers' global namespace)

Tests

  • 4 new test_core tests cover adapter-version rejection paths + hardware capability query
  • All existing tests updated to set .version on their adapter
  • ctest result: 68/70 passing (the 2 failures are pre-existing rac_rag_backend_thread_safety_test compile errors unrelated to this branch)
  • test_core --run-all: 17/17 passing with ASan+UBSan

What's intentionally deferred

  • Phase 7 (per-feature JNI file split): scaffold header jni_shared.h is in place; the actual shard extraction is mechanical and best done in per-feature follow-up commits (each reviewable in isolation).
  • Phase 8 backend retrofit: IInferenceBackend interface exists; the 5 concrete backends inheriting from it is touch-every-backend boilerplate best bundled with the JNI split.
  • CRACommons header fork: Swift maintains its own copy of commons headers; needs a proper sync or an include/ symlink. Tracked.
  • Prebuilt xcframeworks (sdk/runanywhere-swift/Binaries/): need rebuild against the new adapter layout before downstream apps can consume.

Full detail in sdk/runanywhere-commons/CPP_LAYER_CLEANUP_STATUS.md.

Test plan

  • macOS: cmake --preset dev-asan && cmake --build build/dev-asan — clean ASan+UBSan build
  • macOS: runanywhere_commons_jni target builds clean (dev-core preset)
  • macOS: ctest in build/dev-asan — 68/70 pass (2 pre-existing failures unrelated)
  • Swift: swift build — clean
  • Kotlin: JNI target builds on macOS via dev-core
  • React Native: npm run typecheck — clean
  • Flutter: flutter analyze — 0 errors (pre-existing warnings unrelated)
  • Web: my PlatformAdapter.ts changes compile clean
  • CI: iOS XCFramework build (needs CI)
  • CI: Android AAR full build (needs NDK-enabled CI)
  • CI: Linux build (needs CI)
  • CI: WebAssembly build (needs emscripten-enabled CI)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Hardware capability detection API (CPU SIMD, GPU/NPU availability, memory) with human-readable report.
    • Platform adapter ABI versioning to prevent incompatible adapter/runtime mismatches.
    • Wake-word provider plugin system for pluggable wake-word backends.
  • Improvements

    • Marked many public APIs to require checking return status.
    • New backend/integration error codes including ABI incompatibility and JNI-exception.
    • Added development sanitizer presets and test tooling support.

sanchitmonga22 and others added 14 commits April 15, 2026 00:08
Phase 1 of the C++ layer cleanup: puts static-analysis and runtime-sanitizer
infrastructure in place before any production code is touched.

- cmake/Sanitizers.cmake: new module with ENABLE_ASAN/UBSAN/TSAN/MSAN options,
  mutual-exclusion guards, and target_enable_sanitizers(target) helper.
  Maps to -fsanitize=address/undefined/thread/memory with -fno-omit-frame-pointer
  and -g. MSVC uses /fsanitize=address (ASan) and /RTC1 /GS (UBSan substitute).
- CMakePresets.json: add dev-asan (ASan+UBSan, recommended), dev-ubsan, dev-tsan.
  Each preset sets ASAN_OPTIONS/UBSAN_OPTIONS/TSAN_OPTIONS env defaults.
- .clang-tidy: enable cppcoreguidelines-owning-memory, slicing,
  pro-type-{reinterpret-cast,member-init}. Remove the
  -modernize-use-nodiscard disable (we want that check on now that Phase 2
  will add the annotations).
- CMakeLists.txt: include(Sanitizers) after LoadVersions; invoke
  target_enable_sanitizers(rac_commons) right after the add_library call.

Verified on macOS with \`cmake --preset dev-asan && cmake --build build/dev-asan\`
- builds clean with ASan+UBSan enabled.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 2 of the C++ layer cleanup. Adds compile-time enforcement that
return values of all rac_result_t-returning public API functions are
checked by callers, catching silent error drops - the #1 bug source in
C-style API consumption.

- New header include/rac/core/rac_attrs.h provides portable attribute
  macros that degrade gracefully on unsupported compilers:
    RAC_NODISCARD       [[nodiscard]] / warn_unused_result / _Check_return_
    RAC_NONNULL(...)    __attribute__((nonnull(idx))) on GCC/Clang
    RAC_DEPRECATED(msg) [[deprecated(msg)]] / __declspec(deprecated)
    RAC_ATTR_PRINTF     format(printf, fmt_idx, args_idx)
    RAC_NORETURN        [[noreturn]] / _Noreturn / __declspec(noreturn)
    RAC_PURE, RAC_CONST_FN  optimization hints

- rac_types.h now transitively includes rac_attrs.h so every RAC public
  header picks up the macros automatically (every header includes
  rac_types.h either directly or via rac_error.h).

- All 479 `RAC_API rac_result_t <func>(` declarations across 66 headers
  updated to `RAC_API RAC_NODISCARD rac_result_t <func>(`. Applied via
  a single sed pass for uniformity. This includes the init/register/
  load/generate/transcribe/synthesize functions across every feature
  (LLM, STT, TTS, VAD, VLM, diffusion, RAG, wakeword, voice_agent),
  infrastructure (network, download, model registry, telemetry),
  and backends (LlamaCPP, ONNX, WhisperCPP, WhisperKit CoreML).

- Explicit enum underlying types (e.g. `typedef enum : int32_t`)
  intentionally NOT added in this pass - the C23 syntax is not portable
  to older C toolchains used by downstream consumers. All enums already
  default to 32-bit int on every supported target platform; mismatch
  risk is theoretical. Can be revisited once C23 support is universal.

Verified on macOS (dev-asan preset): rac_commons builds cleanly.
Build surfaces ~40 pre-existing silent return-value drops across
lifecycle_manager / download_orchestrator / component files as warnings.
These are not fixed in this commit (each requires per-site judgement of
whether to log, propagate, or intentionally ignore) but the nodiscard
safety net is now in place to prevent new instances.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…NI error code

Phase 3 of the C++ layer cleanup. Addresses verified correctness issues
surfaced by the audit plus follow-up TSan false-positive prevention.

Changes:

1. include/rac/core/rac_error.h
   - New error code RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION (-605) for
     runtime backend ABI mismatches.
   - New error code RAC_ERROR_JNI_EXCEPTION (-606) for pending-exception
     cleanup (will be used in Phase 6's JniScope).

2. src/backends/onnx/rac_backend_onnx_register.cpp
   - rac_backend_onnx_register() now queries SherpaOnnxGetVersionStr() at
     registration time, parses major.minor.patch, and refuses to register
     if the prebuilt sherpa-onnx binary's major.minor does not match the
     compile-time expectation (1.12.x per VERSIONS file).
   - Previously: mismatch -> SIGSEGV on first inference (struct layout
     divergence). Now: clean error code, loud log, no crash.
   - Includes `sherpa-onnx/c-api/c-api.h` only when SHERPA_ONNX_AVAILABLE
     (header macro already used pervasively in onnx_backend.cpp).

3. src/infrastructure/download/download_manager.cpp
   - `bool is_healthy` / `bool is_paused` -> `std::atomic<bool>` with
     explicit member initializers. These fields are read from the hot
     path (e.g. start_download consults is_paused before acquiring the
     mutex) and written by pause/resume on arbitrary threads. Without
     atomic, TSan flags every access as a data race; on weak-memory
     architectures (arm64) this could theoretically cause torn reads.
   - No behavioural change; pure correctness fix.

4. tests/CMakeLists.txt
   - rac_link_archive_deps() helper now also calls
     target_enable_sanitizers() so test executables link the ASan/UBSan
     runtime when those options are on globally. Fixes
     "___asan_after_dynamic_init undefined" link errors seen with the
     dev-asan preset.

Intentionally NOT included in this commit:

- WhisperKit CoreML "memory leak" flagged in the audit: re-verification
  showed rac_stt_destroy() in src/features/stt/rac_stt_service.cpp:159
  already frees both the service wrapper (via free(service)) and the
  strdup'd model_id (via free(const_cast<char*>(service->model_id))),
  and is invoked from stt_component.cpp:115,130 on normal and error
  paths. The original malloc+strdup pattern is consistent with the
  cleanup; converting to new+std::string would introduce a double-free
  without also migrating rac_stt_destroy. Audit was incorrect; no
  action needed.

- HTTP client realloc pattern (http_client.cpp:101): realloc failure is
  silently dropped, but the caller (rac_http_request_add_header) is
  declared void with no way to propagate the error. Behaviour pre-dates
  this branch and is functionally OK. Needs a larger API-breaking
  change to fix properly; deferred.

- The 7 wakeword_service.cpp TODOs are Phase 5 work (service-to-backend
  wiring). The 5 JNI TODOs are Phase 6 work (systematic exception-check
  sweep). The 2 llama.cpp TODOs and 1 model_types deep-copy TODO are
  low-priority deliberate notes and remain as-is.

Verified on macOS (dev-asan preset): full build including all tests
completes successfully. ASan + UBSan instrumentation active.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 4 of the C++ layer cleanup. Resolves PR #383 review comment by
moving the Windows POSIX-compatibility shim from the public include path
to src/internal/, preventing un-prefixed global names from leaking into
SDK consumers.

Background:
  include/rac/core/rac_platform_compat.h defined `DIR`, `dirent`,
  `opendir`, `readdir`, `closedir`, `strcasecmp`, `strncasecmp`, and
  several `S_IS*` / `S_IFLNK` macros at the global scope on Windows so
  the rest of the codebase could use them as POSIX without #ifdefs.
  Because this header lived under `include/`, every downstream consumer
  that pulled in a commons public header (transitively) inherited these
  un-prefixed names, violating the project rule that all public symbols
  must be `rac_` prefixed. Flagged in PR #383 but deferred.

Changes:
- Moved:    include/rac/core/rac_platform_compat.h
            -> src/internal/rac_platform_compat.h
- Updated 10 call sites to use the new path:
    src/backends/onnx/onnx_backend.cpp
    src/backends/onnx/wakeword_onnx.cpp
    src/features/rag/onnx_embedding_provider.cpp
    src/features/vlm/vlm_component.cpp
    src/infrastructure/download/download_orchestrator.cpp
    src/infrastructure/extraction/rac_extraction.cpp
    tests/test_config.h
    tests/test_download_orchestrator.cpp
    tests/test_extraction.cpp
  Each now does `#include "internal/rac_platform_compat.h"` (no
  `rac/core/` prefix). The PRIVATE include root for these translation
  units is the src/ dir.

- CMakeLists wiring:
    * src/backends/onnx/CMakeLists.txt: new PRIVATE include of src/ for
      rac_backend_onnx.
    * src/features/rag/CMakeLists.txt: new PRIVATE include of src/ for
      rac_backend_rag.
    * tests/CMakeLists.txt: rac_link_archive_deps() helper now adds src/
      to the PRIVATE include path (and ensures target_enable_sanitizers
      gets applied too). Applied the helper to all test executables that
      previously skipped it (test_vad, test_stt, test_tts, test_wakeword,
      test_llm, test_voice_agent, rac_benchmark_tests, rac_chunker_test,
      rac_simple_tokenizer_test, rac_rag_backend_thread_safety_test).

- Header docstring updated: the deferred-TODO block is replaced with
  a post-move status note explaining the file's new internal status.

Not a public ABI change; rac_commons.a contents and exports unchanged.
The move takes effect purely at preprocess/compile time.

Verified on macOS (dev-asan preset): rac_commons + all 12 consumer-side
test executables build clean. (The pre-existing
rac_rag_backend_thread_safety_test failure - missing IEmbeddingProvider
class in tests/rag_backend_thread_safety_test.cpp - is unrelated to
this commit; it predates the branch, surfaced here only because our
sanitizer-runtime linking now makes tests that previously didn't
attempt a full link surface their pre-existing compile errors.)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 5 of the C++ layer cleanup. Eliminates the seven stub TODOs in
src/features/wakeword/wakeword_service.cpp and makes wake-word detection
actually functional (no runtime model/inference testing in this commit -
see below).

The audit flagged wakeword_service.cpp as a skeleton: it accepted
load_model / load_vad / process / reset / unload calls and returned
RAC_SUCCESS but never actually invoked any inference. Meanwhile the
ONNX wake-word backend already existed in full
(src/backends/onnx/wakeword_onnx.cpp, 1056 LOC of real ONNX Runtime +
Silero VAD code). The two just weren't connected - rac_commons can't
link rac_backend_onnx directly because rac_backend_onnx is optional
and gets pulled in only when RAC_BACKEND_ONNX=ON.

Design:
- New provider-vtable pattern in include/rac/features/wakeword/
  rac_wakeword_service.h (`rac_wakeword_provider_ops_t`). The concrete
  backend implements this vtable and installs it via
  `rac_wakeword_provider_set()`; the service layer stores the pointer in
  a `std::atomic<const rac_wakeword_provider_ops_t*>` and dispatches
  every wake-word call through it. Same design as the existing
  whisperkit-coreml callbacks, generalised.

- New public API:
    rac_wakeword_provider_set(const rac_wakeword_provider_ops_t*)
    rac_wakeword_has_provider(void) -> rac_bool_t
  Both exported via RAC_API.

- wakeword_service.cpp updates:
    * initialize(): calls provider->create() to get a backend handle;
      stores it on the service. If no provider is registered the
      service still initialises (degraded mode, logs a WARNING).
    * load_model(): provider->load_model() must succeed before the model
      is entered into the service's in-memory roster. Backward-compat
      quirk: if NO provider is registered, the roster entry is still
      recorded but marked is_loaded=false so subsequent process() won't
      generate spurious detections.
    * load_vad(): mirror of load_model.
    * unload_model(): forwards to provider->unload_model, then deletes
      the service-layer entry whether or not the backend call succeeded.
    * process(): replaces the three TODO-inference-placeholders with a
      single call to provider->process, which returns the detection
      index, confidence, and VAD state in one shot. The prior
      "simulate with placeholder" stub is gone.
    * reset(): forwards to provider->reset.
    * set_threshold(): pushed through to provider->set_threshold.
    * destroy(): calls provider->destroy on the backend handle before
      deleting the service.

- wakeword_onnx.cpp updates:
    * New file-local adapter functions that translate between the
      generic provider vtable and the ONNX-specific
      rac_wakeword_onnx_* API. Handles the config translation
      (frame_length_ms -> frame_length samples) and the expanded
      process signature (process_with_vad returns VAD data too).
    * Static const `g_onnx_wakeword_provider_ops` vtable.
    * rac_backend_wakeword_onnx_register() now installs this vtable via
      rac_wakeword_provider_set() (and clears it on unregister).

Concurrency:
- g_provider is atomic; writes (provider_set) use memory_order_release,
  reads (every dispatch in the hot path) use memory_order_acquire.
  Expected usage is write-once at SDK startup; runtime re-registration
  is supported but will not update services that already finished
  `initialize()` (their backend_handle was created through the prior
  provider).

Follow-up work not in this commit:
- The openWakeWord embedding/melspec shared models are wired via the
  ONNX-specific `rac_wakeword_onnx_init_shared_models()` call today.
  The generic config struct doesn't expose them. Phase 9 (struct
  versioning) will add these as new fields.
- Runtime validation requires real `.onnx` model files and a real
  audio device. All the plumbing compiles clean (macOS dev-asan full
  build incl. test_wakeword), but end-to-end "hey jarvis"-fires-event
  testing requires hardware I can't exercise in this session. User
  should verify on Android/iOS test devices before merging.

Verified: rac_commons + rac_backend_onnx + test_wakeword build clean
with ASan + UBSan instrumentation on macOS.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…orm adapter

Phase 6 of the C++ layer cleanup. Introduces the foundation for fixing the
~7% JNI exception-check coverage (11 checks across 149 entry points) flagged
in the audit, and demonstrates the pattern on the highest-frequency
adapter callbacks.

New infrastructure:

- src/jni/jni_scope.h (new, internal)
  * `rac::jni::Local<T>`: move-only RAII wrapper around a local jobject
    ref. Deletes the local on scope exit. Implicitly convertible to the
    underlying jobject so it drops into existing APIs.
  * `rac::jni::JniScope`: stack-scoped helper that auto-checks for
    pending Java exceptions after every wrapped JNIEnv call, logs via
    ExceptionDescribe, clears via ExceptionClear, and latches an
    internal error state so callers can bail with a single `s.failed()`
    check. Provides typed wrappers for `NewStringUTF`, `NewByteArray`,
    `CallObjectMethod`, `CallBooleanMethod`, `CallIntMethod`,
    `CallLongMethod`, `CallVoidMethod`.
  * `RAC_JNI_TRY(scope)` / `RAC_JNI_TRY_PTR` / `RAC_JNI_TRY_VOID`:
    compact early-return macros for the common "bail if failed" pattern.

- Design goals: zero overhead when no exception fires (the hot path is
  one atomic-free branch), drop-in with existing `getJNIEnv()` callers,
  and no change to the external JNI ABI.

Sweep applied in this commit:

- src/jni/runanywhere_commons_jni.cpp: three platform adapter callbacks
  converted to JniScope. These are the highest-frequency JNI sites
  (every file operation and every native log line routes through them):
    * jni_log_callback              - 2 NewStringUTF + CallVoidMethod
    * jni_file_exists_callback      - 1 NewStringUTF + CallBooleanMethod
    * jni_file_read_callback        - 1 NewStringUTF + CallObjectMethod
                                       + GetArrayLength + GetByteArrayRegion
  Plus the free(*out_data) on GetByteArrayRegion exception path - the
  previous code would have leaked `malloc`'d memory if the JNI call
  raised after allocation. Now the free runs under s.failed().

Remaining sweep (follow-up work):

- 146 of the original 149 JNIEXPORT entry points still need the same
  treatment. The pattern is mechanical: `JniScope s(env, "<func>");
  auto jStr = s.new_string_utf(...); RAC_JNI_TRY(s); ...`. The file
  should be converted alongside Phase 7's per-feature split so each
  feature file can be swept in isolation.

Verified: jni_scope.h is header-only so it pulls in cleanly wherever
jni.h is already on the include path. The JNI bridge only builds when
RAC_BUILD_JNI=ON (Android AAR build); that build is not configured in
the local dev-asan preset, but the changes are standard C++ and will
compile identically under NDK clang.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 8 of the C++ layer cleanup. Adds two additive, non-breaking pieces
of architectural foundation that Phase 10+ can build on.

1. include/rac/core/rac_hardware.h + src/core/rac_hardware.cpp
   Public C API for querying hardware accelerators at runtime:
     rac_hardware_query_capabilities(rac_hardware_report_t* out)

   The rac_hardware_report_t struct is versioned (RAC_HARDWARE_REPORT_V1)
   and carries boolean flags for CPU SIMD (NEON / SSE4.2 / AVX / AVX2 /
   AVX-512), GPU stacks (Metal, CUDA, Vulkan, OpenCL, WebGPU), NPUs (ANE,
   Qualcomm QNN, MediaTek Genio), and platform hints (Apple Silicon,
   iOS Simulator, Android Emulator). Each flag is compile-time-honest:
   a commons binary built without -DGGML_USE_METAL reports has_metal =
   RAC_FALSE even on an Apple Silicon host, because the Metal code path
   wasn't linked in.

   Implementation uses compile-time constexpr flags plus light runtime
   probes (`sysctlbyname hw.physicalcpu` / `sysconf _SC_NPROCESSORS_ONLN`
   / `hw.memsize * 3/4` for Apple Silicon unified memory estimate).
   Never throws, never blocks, never fails (beyond version mismatch).

   Plus a debug helper `rac_hardware_format_report(report)` that returns
   a newly-allocated multi-line summary string (rac_strdup; caller frees
   via rac_free).

2. src/backends/backend_interface.h (internal, not installed)
   Defines a small abstract class `rac::backends::IInferenceBackend`
   with the cross-capability lifecycle contract every backend has
   separately reimplemented so far:
     - advertise() -> BackendAdvertisement (name, framework, priority)
     - supports_hardware(report) -> rac_bool_t (default TRUE)
     - load_model(path, config_json)
     - unload_model()
     - cancel()
     - health_check() -> BackendHealth

   Capability-specific operations (generate_text, transcribe_audio, ...)
   stay where they are - in the per-capability vtables. The abstract
   class is deliberately lifecycle-only to avoid a god-class.

   New backends are expected to inherit from IInferenceBackend; the five
   existing backends (LlamaCPP, ONNX, WhisperCPP, MetalRT, WhisperKit
   CoreML) will be retrofitted incrementally. That retrofit is not done
   in this commit because it's touch-every-file boilerplate with no
   observable change; it's tracked as a follow-up in CPP_LAYER_AUDIT.md.

   Registration helpers (backend_registry.h - "a single registrar instead
   of 5 per-capability boilerplate blocks") likewise remain as follow-up.

CMakeLists: added src/core/rac_hardware.cpp to RAC_CORE_SOURCES.

Verified on macOS dev-asan preset: rac_commons builds clean with the
new source. rac_hardware.cpp's compile-time SIMD flags correctly reflect
the Apple-Silicon-arm64 host (has_neon=TRUE, has_metal=gated on build
flag, has_ane=TRUE).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 9 of the C++ layer cleanup. Makes the adapter struct forward-
compatible so new callbacks can be appended without breaking older
SDK consumers.

Schema change:
- include/rac/core/rac_platform_adapter.h
  * New constant RAC_PLATFORM_ADAPTER_VERSION (currently 1).
  * New leading field `uint32_t version` on rac_platform_adapter_t.
    Must be set by caller to RAC_PLATFORM_ADAPTER_VERSION.

Runtime check:
- src/core/rac_core.cpp rac_init():
  * `adapter->version == 0` -> treated as legacy / pre-versioning;
    logs a WARNING and accepts for this release (backward-compat
    grace period).
  * `adapter->version > RAC_PLATFORM_ADAPTER_VERSION` -> caller was
    compiled against a newer struct layout than the runtime supports;
    rejects with RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION and sets
    error details.
  * `1 <= version <= RAC_PLATFORM_ADAPTER_VERSION` -> accepted
    silently. Today only v1 exists, so all valid.

SDK-consumer impact:
- Old C callers using `rac_platform_adapter_t a = {0}; ...` work as
  before, with one WARNING log at init time. Action required: add
  `.version = RAC_PLATFORM_ADAPTER_VERSION` to the zero-init literal.
- Swift / Kotlin / RN / Flutter / WASM wrappers need a similar one-
  liner. Phase 10 (deferred in this branch - see CPP_LAYER_AUDIT.md)
  will land those wrapper-side updates.

Why version-first-field not tail-padding:
  The version field is the FIRST field because it must be readable
  even when the rest of the struct layout is ambiguous (e.g. a v3
  caller hits a v1 runtime: the runtime can read `version` at
  offset 0 with confidence, inspect it, and either accept or reject).
  Trailing padding alone can't rescue that scenario because the
  runtime has no way to know the caller's struct size.

Further versioning of config structs (rac_config_t,
rac_llm_config_t, rac_stt_config_t, ...) follows the same pattern
and is documented as follow-up in CPP_LAYER_AUDIT.md. They're lower
priority than the platform adapter because they're set per-component
rather than once at SDK init, and the failure mode of a wrong field
is a mis-configured component (recoverable) rather than crashed
platform callbacks (undefined behaviour).

Verified: rac_commons builds clean with ASan + UBSan on macOS. The
WARNING path and REJECT path of the version check were validated by
hand-building adapters with version=0 and version=999 respectively.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Final summary document for the smonga/cpp-layer-full-cleanup branch.
Lists all 8 commits with per-commit scope, what compiles clean, what's
deferred and why, and recommended next steps for a follow-up session.

Also corrects the audit's original claim about 18k/16k-line backend
files (actual sizes are 1.5k/1.3k) and notes the three phases that are
intentionally deferred: Phase 7 (file splits - not needed), Phase 10
(5 SDK wrapper updates - scope too large and protected by version grace
period), Phase 11 (multi-platform builds - requires NDK/Emscripten/
Windows VM not available locally).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Completes Phase 6 of the C++ layer cleanup by applying the JniScope RAII
helper to every C -> Java callback path in runanywhere_commons_jni.cpp.
Also tightens the platform adapter version check per user request: no
more version=0 grace period, every caller must explicitly set
`.version = RAC_PLATFORM_ADAPTER_VERSION`.

JniScope applied to these callbacks:
  jni_log_callback                   (was done in prior commit)
  jni_file_exists_callback           (was done in prior commit)
  jni_file_read_callback             (was done in prior commit)
  jni_file_write_callback            NEW
  jni_file_delete_callback           NEW
  jni_secure_get_callback            NEW
  jni_secure_set_callback            NEW
  jni_secure_delete_callback         NEW
  jni_now_ms_callback                NEW
  llm_stream_callback_token          NEW (was manual ExceptionCheck)
  jni_device_get_info                NEW (was manual ExceptionCheck)
  jni_device_get_id                  NEW (was manual ExceptionCheck)
  jni_device_is_registered           NEW (was manual ExceptionCheck)
  jni_device_set_registered          NEW (was manual ExceptionCheck)
  jni_device_http_post               NEW (was manual ExceptionCheck)
  jni_telemetry_http_callback        NEW (was manual ExceptionCheck)
  model_assignment_http_get_callback NEW (was manual ExceptionCheck)

All 17 native-to-Java callback sites now have uniform exception safety
via JniScope. Manual ExceptionCheck blocks have been removed - JniScope
handles check+describe+clear inside its wrapped methods, so the callback
bodies are substantially shorter and harder to get wrong.

Remaining unprotected NewStringUTF sites are `return env->NewStringUTF(...)`
patterns inside JNIEXPORT functions (Java -> native direction). These are
safe as-is: the JVM propagates pending exceptions correctly through
JNIEXPORT return. JniScope is not needed in that direction.

rac_init version check:
- Removed the backward-compat warning for `adapter.version == 0`. Now
  rejects with RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION immediately and
  sets a clear error-details string pointing the caller at the fix.
- Phase 10 SDK consumer updates (next commits) will set .version on
  every caller.

Pre-existing build issue unrelated to this commit:
  Four `cannot initialize 'void **' with 'JNIEnv **'` errors at lines
  ~127, 768, 2464, 4187 are a macOS-host-JDK vs Android-NDK typing
  difference that predates this branch. On Android NDK builds the
  JNIEnv** -> void** conversion is implicit; on macOS host JDK it
  requires an explicit reinterpret_cast. These errors exist on `main`
  just as much and are tracked as separate follow-up (touch points in
  `getJNIEnv()`, the LLM streaming attach path, the model-assignment
  attach path, and the webhook attach path).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The JNI spec types AttachCurrentThread's second parameter as `JNIEnv**`
on Android NDK and `void**` on macOS/Linux host JDKs. Previously the
code passed `&env` raw (a `JNIEnv**`), which compiled cleanly on
Android but failed on macOS host with
  "cannot initialize a parameter of type 'void **' with an rvalue of
   type 'JNIEnv **'".

Fixed at all 4 call sites (getJNIEnv, llm_stream_callback_token,
model_assignment_http_get_callback, another LLM streaming path) by
casting explicitly through `reinterpret_cast<void**>`. The cast is
layout-safe on every supported platform because `void**` and `JNIEnv**`
have the same representation.

Side benefit: the `runanywhere_commons_jni` target now builds cleanly
on the macOS dev-core preset, unblocking local testing of JNI changes
without needing a full Android NDK round-trip.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… all 5 SDKs

Phase 10 of the C++ layer cleanup. Updates every SDK wrapper to set the
versioned-ABI field introduced in Phase 9 so rac_init() stops rejecting
their platform adapters.

Swift (sdk/runanywhere-swift):
- Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+PlatformAdapter.swift
  adapter.register() now sets `adapter.version = RAC_PLATFORM_ADAPTER_VERSION`
  right after the zero-init and before wiring any callbacks.
- Sources/RunAnywhere/CRACommons/include/rac_platform_adapter.h:
  Added the `RAC_PLATFORM_ADAPTER_VERSION ((uint32_t)1)` macro AND the
  `uint32_t version;` leading field on rac_platform_adapter_t, synced
  from sdk/runanywhere-commons/include/. (CRACommons is a manually
  maintained header fork today - the build-swift.sh sync only covers
  backend headers.)
- Sources/RunAnywhere/CRACommons/include/rac_error.h:
  Added the three new error codes (RAC_ERROR_BACKEND_UNAVAILABLE,
  RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION, RAC_ERROR_JNI_EXCEPTION).
  Verified `swift build` completes cleanly.

Kotlin / JNI (sdk/runanywhere-commons/src/jni):
- runanywhere_commons_jni.cpp:470 - racSetPlatformAdapter now sets
  `g_c_adapter.version = RAC_PLATFORM_ADAPTER_VERSION;` when populating
  the C-side adapter struct that bridges Kotlin callbacks into native
  callable function pointers. Kotlin itself doesn't need any change
  since Kotlin hands its adapter object to JNI, and JNI builds the
  C struct.
- Verified: `cmake --build build/dev-core --target runanywhere_commons_jni`
  succeeds on macOS dev-core preset.

React Native (sdk/runanywhere-react-native):
- packages/core/cpp/bridges/InitBridge.cpp:948 - registerPlatformAdapter
  zero-inits then sets `adapter_.version = RAC_PLATFORM_ADAPTER_VERSION`.
  No TypeScript-side changes needed - the TS bridge just calls through
  to this C++ setup.

Flutter (sdk/runanywhere-flutter):
- packages/runanywhere/lib/native/ffi_types.dart - added leading
  `@Uint32() external int version;` field to RacPlatformAdapterStruct
  matching the C struct layout. Exposed
  `const int racPlatformAdapterVersion = 1;` for consumer use.
- packages/runanywhere/lib/native/dart_bridge_platform.dart - after
  calloc<RacPlatformAdapterStruct>() and before wiring callbacks,
  sets `adapter.ref.version = racPlatformAdapterVersion`.

Web (sdk/runanywhere-web):
- packages/llamacpp/src/Foundation/PlatformAdapter.ts - the hand-rolled
  struct serializer that writes function pointers into the WASM-side
  adapter now first writes the 4-byte version uint32 at offset 0,
  shifts the subsequent pointer offsets accordingly, and includes the
  constant `RAC_PLATFORM_ADAPTER_VERSION = 1`.

With this commit rac_init()'s strict version check (introduced in
commit d4a5c9f, which rejects version=0 adapters with
RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION and no grace period) will accept
every SDK's adapter out of the box.

Followup (not in this commit):
- CRACommons is manually forked; the sync script in build-swift.sh
  currently only copies the whisperkit_coreml backend header. A full
  commons-to-CRACommons sync pass is needed one-off, or better, the
  fork should be dropped in favour of a direct include of
  ../runanywhere-commons/include. Tracked in CPP_LAYER_CLEANUP_STATUS.md.
- Corresponding backend xcframeworks shipped under Binaries/ were
  built against the pre-versioning header. They'll need a rebuild
  before downstream apps can consume this branch's changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…split scaffold

Adds test coverage for the new invariants introduced in Phase 3 / 8 / 9
and creates the shared-state header that will anchor the Phase 7
per-feature JNI file split.

tests/test_core.cpp:
- Updated the existing static test_adapter to set
  `.version = RAC_PLATFORM_ADAPTER_VERSION` (required since commit
  d4a5c9f removed the version=0 grace period). Without this, every
  test that calls rac_init would fail with
  RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION.
- New test_adapter_version_zero_rejected: verifies rac_init returns
  RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION when adapter.version = 0 AND
  doesn't leave the SDK in an initialised state.
- New test_adapter_version_future_rejected: verifies rac_init rejects
  version > RAC_PLATFORM_ADAPTER_VERSION, catching future-compiled
  callers against an older runtime.
- New test_hardware_query: validates rac_hardware_query_capabilities
  fills the struct correctly on the running platform. Confirms at least
  one logical CPU detected, and on arm64 macOS confirms is_apple_silicon,
  has_neon, has_ane all report TRUE.
- New test_hardware_wrong_version: negative case for the hardware API -
  version = 0 is refused with RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION.

Results: `./test_core --run-all` reports 17 passed / 0 failed
(previously 13 tests; added 4 new ones). Full ctest suite:
68 passed / 2 failed, with the 2 failures being the pre-existing
rac_rag_backend_thread_safety_test that doesn't compile due to a
missing IEmbeddingProvider class (unrelated to this branch).

src/jni/jni_shared.h (new):
- Skeleton header declaring the shared state + helpers every future
  JNI shard will need: JavaVM*, g_platform_adapter, cached method IDs,
  getJNIEnv(), getCString(). Today the definitions remain `static` in
  runanywhere_commons_jni.cpp; the comments describe the planned shard
  layout (jni_platform_adapter.cpp, jni_llm.cpp, jni_stt.cpp, ...).
- Pulls in jni_scope.h + rac_core/rac_error/rac_logger/rac_platform_adapter
  headers + the LOGi/d/w/e Android log macros so every shard can include
  this one header and get a ready-to-use JNI environment.
- The actual per-feature .cpp extraction is deliberately left for
  follow-up commits - doing all 4,800 lines in one diff would be
  unreviewable. Scaffolding is in place to do it incrementally.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 15, 2026

Too many files changed for review. (104 files found, 100 file limit)

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 15, 2026

📝 Walkthrough

Walkthrough

Adds sanitizer tooling, a portable attribute header, RAC_NODISCARD annotations across many public APIs, platform-adapter ABI/versioning with runtime validation, a hardware-capabilities API, JNI RAII/exception-safety utilities, a wakeword provider vtable, backend ABI checks, internal header relocation, and related consumer SDK updates and tests.

Changes

Cohort / File(s) Summary
Build/tooling
sdk/runanywhere-commons/.clang-tidy, sdk/runanywhere-commons/CMakeLists.txt, sdk/runanywhere-commons/CMakePresets.json, sdk/runanywhere-commons/cmake/Sanitizers.cmake
Add Sanitizers.cmake and ENABLE_* options, new dev-asan/ubsan/tsan presets, sanitizer enable helper, and tighten clang-tidy checks.
Portable attributes
sdk/.../include/rac/core/rac_attrs.h, sdk/.../include/rac/core/rac_types.h, sdk/runanywhere-swift/.../include/rac_attrs.h, sdk/runanywhere-swift/.../include/rac_types.h
Introduce RAC_NODISCARD, RAC_NONNULL, RAC_DEPRECATED, RAC_NORETURN, RAC_ATTR_PRINTF, RAC_PURE, etc., and include them widely.
RAC_NODISCARD sweep (public headers)
sdk/runanywhere-commons/include/rac/... (many files across core, features, backends, infrastructure, platform, utils)
Applied RAC_NODISCARD to ~180+ public rac_result_t-returning APIs across lifecycle, services, components, analytics, events, backends, infrastructure, and platform headers.
Hardware capabilities API
sdk/runanywhere-commons/include/rac/core/rac_hardware.h, sdk/runanywhere-commons/src/core/rac_hardware.cpp
Add rac_hardware_report_t, RAC_HARDWARE_REPORT_V1, rac_hardware_query_capabilities() and rac_hardware_format_report().
Platform-adapter ABI/versioning
sdk/runanywhere-commons/include/rac/core/rac_platform_adapter.h, sdk/runanywhere-commons/src/core/rac_core.cpp, consumer SDK files...
Add uint32_t version as first field, RAC_PLATFORM_ADAPTER_VERSION, runtime validation in rac_init() rejecting version==0 or > supported.
Error codes
sdk/runanywhere-commons/include/rac/core/rac_error.h, Swift variant
Add RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION and RAC_ERROR_JNI_EXCEPTION (and Swift also adds BACKEND_UNAVAILABLE).
JNI RAII & refactor
sdk/runanywhere-commons/src/jni/jni_scope.h, .../jni/jni_shared.h, .../jni/runanywhere_commons_jni.cpp
New rac::jni::JniScope and Local<T> RAII wrappers; refactor native→Java callbacks to use JniScope, adjust AttachCurrentThread usage, and handle JNI exceptions centrally.
Wakeword provider vtable
sdk/runanywhere-commons/include/rac/features/wakeword/rac_wakeword_service.h, .../src/features/wakeword/wakeword_service.cpp, .../src/backends/onnx/wakeword_onnx.cpp
Add rac_wakeword_provider_ops_t, rac_wakeword_provider_set/rac_wakeword_has_provider, make service dispatch to provider, implement ONNX provider adapter.
Backend ABI validation & interface
sdk/runanywhere-commons/src/backends/onnx/rac_backend_onnx_register.cpp, sdk/runanywhere-commons/src/backends/backend_interface.h
Parse/check Sherpa-ONNX version at runtime (require major/minor match), add IInferenceBackend abstract interface for backend lifecycle/health.
Internal header relocation
sdk/runanywhere-commons/src/internal/rac_platform_compat.h, many source includes
Move platform-compat header into src/internal/ and update includes across sources/tests to internal/rac_platform_compat.h.
Download manager concurrency
sdk/runanywhere-commons/src/infrastructure/download/download_manager.cpp
Switch health/pause flags to std::atomic<bool> for concurrent access.
Tests & CMake test helpers
sdk/runanywhere-commons/tests/CMakeLists.txt, sdk/runanywhere-commons/tests/test_core.cpp, other tests
Enable sanitizer flags for test targets, include src/ in test includes, add tests for adapter version validation and hardware query.
Consumer SDK changes
Flutter, React Native, Swift, Web files (e.g., dart_bridge_platform.dart, ffi_types.dart, InitBridge.cpp, Swift/TS adapters)
Initialize adapter.version in multiple platform bridges and update FFI/types to include the version field.
Docs
sdk/runanywhere-commons/CPP_LAYER_CLEANUP_STATUS.md
Add documentation summarizing C++ cleanup, ABI/versioning, JNI sweep, wakeword provider, and sanitizer/tooling steps.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • Siddhesh2377

"🐰
I hop and patch with gentle paws,
Version set and tidy laws,
JNI scopes hold tight,
Sanitizers take flight,
API petals gleam in audit's draws."

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch smonga/cpp-layer-full-cleanup

sanchitmonga22 and others added 2 commits April 15, 2026 11:58
… ONNX, ETC)

Follow-up audit revealed that my Phase 2 sed only targeted
`^RAC_API rac_result_t`, missing the per-backend API macros
(`RAC_LLAMACPP_API`, `RAC_ONNX_API`, `RAC_BACKEND_RAG_API`,
`RAC_BACKEND_METALRT_API`). Added `RAC_NODISCARD` to 59 additional
declarations across 8 backend headers:

  rac_llm_llamacpp.h       17 functions
  rac_wakeword_onnx.h      11 functions
  rac_vad_onnx.h            8 functions
  rac_vlm_llamacpp.h        8 functions
  rac_stt_onnx.h            5 functions
  rac_stt_whispercpp.h      5 functions
  rac_tts_onnx.h            3 functions
  rac_backend_metalrt.h     2 functions

Also fixed my own omission: rac_wakeword_provider_set() in
rac_wakeword_service.h (added in Phase 5) didn't have RAC_NODISCARD.

Verified: rac_commons + test_core still build cleanly, all 17 test_core
tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Follow-up sweep after the primary refactor, driven by three sequential
audit agents. Findings that turned out to be real, grouped:

Agent #1 (completeness):
- Missing RAC_NODISCARD on backend-specific API macros (RAC_LLAMACPP_API,
  RAC_ONNX_API, etc.) - addressed in commit 4630546 (previous).
- Missing RAC_NODISCARD on rac_wakeword_provider_set (my own
  Phase 5 addition) - fixed in this commit.

Agent #2 (threading / UB):
- wakeword provider lifecycle race: std::atomic<T*> only handles the
  pointer swap, not the lifetime of the pointed-to ops struct. Fixed
  by documenting the threading contract on rac_wakeword_provider_set:
  register exactly once at SDK init, before any service is created;
  ops must be file-scope static; NULL is only legal at teardown.
- Other claims (ReleaseStringUTFChars ordering in jni_device_get_info /
  jni_secure_get_callback, reinterpret_cast strict aliasing in
  getJNIEnv) re-verified: the claims were wrong. No changes.

Agent #3 (integration):
- CRITICAL: CRACommons header fork was missing rac_attrs.h (new) and
  rac_hardware.h (new), and CRACommons.h umbrella header didn't expose
  them. Copied both files into
  sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/ with
  the flat-include path convention that CRACommons uses (`#include
  "rac_types.h"` vs commons's `#include "rac/core/rac_types.h"`).
- CRACommons.h umbrella now lists rac_attrs.h alongside rac_types.h
  and rac_hardware.h alongside rac_audio_utils.h.
- CRACommons rac_types.h now transitively includes rac_attrs.h so
  RAC_NODISCARD etc. are visible to every other header that includes
  it (matching the commons behaviour from Phase 2).
- Verified Swift SDK (`swift build`) still compiles cleanly with the
  synced headers.

Also added in this commit:
- Two compile-time static_asserts in src/core/rac_core.cpp that lock
  the layout of rac_platform_adapter_t: `offsetof(version) == 0` and
  `sizeof(version) == sizeof(uint32_t)`. Any future reorder or type
  change fails at compile time instead of silently misaligning the
  adapter struct read by foreign-language wrappers. This is the
  cheapest safety net against the "Dart FFI padding vs C struct
  padding" and "WASM offset math vs sizeof_platform_adapter" concerns
  agent #3 flagged.

Not fixed in this commit (tracked as documented follow-up):
- xcframework headers in `sdk/runanywhere-commons/dist/*.xcframework/
  */Headers/` are committed prebuilt binaries from before the
  refactor. Regenerating them needs CI (requires running the
  build-ios.sh pipeline). User explicitly said "disregard CI/CD" for
  this session.
- RN CMakeLists.txt / Gradle header vs .so synchronization: lives in
  the RN package's build pipeline, separate branch-able concern.

Verified: rac_commons + test_core build clean; 17/17 test_core pass;
Swift SDK builds clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
sdk/runanywhere-commons/tests/CMakeLists.txt (1)

275-287: ⚠️ Potential issue | 🟡 Minor

Duplicate FetchContent_Declare for GoogleTest with conflicting versions.

FetchContent_Declare(googletest ...) is called twice:

  • Line 193-198: release-1.12.1 for RAG pipeline tests
  • Line 277-281: v1.14.0 for benchmark tests

CMake's FetchContent uses the first declaration for a given name and ignores subsequent ones. The benchmark tests will actually use GoogleTest 1.12.1, not 1.14.0 as intended.

Consider consolidating to a single version or using distinct content names if different versions are truly required.

Option 1: Consolidate to single version (recommended)

Move GoogleTest fetch to the top of the file before any test definitions, and use one consistent version (e.g., v1.14.0):

+# =============================================================================
+# GoogleTest (shared by all test suites)
+# =============================================================================
+include(FetchContent)
+FetchContent_Declare(
+    googletest
+    GIT_REPOSITORY https://github.com/google/googletest.git
+    GIT_TAG v1.14.0
+    GIT_SHALLOW TRUE
+)
+set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
+set(INSTALL_GTEST OFF CACHE BOOL "" FORCE)
+FetchContent_MakeAvailable(googletest)
+include(GoogleTest)
+
 # =============================================================================
 # Integration Test Suite for runanywhere-commons
 # =============================================================================

Then remove the duplicate FetchContent_Declare blocks from lines 189-201 and 275-287.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/tests/CMakeLists.txt` around lines 275 - 287, There
are two FetchContent_Declare blocks for the same content name "googletest" with
different GIT_TAGs (release-1.12.1 and v1.14.0), so CMake will honor the first
and ignore the second; to fix, consolidate to a single FetchContent_Declare for
googletest with the desired version (pick one GIT_TAG, e.g., v1.14.0), move that
single declaration (and FetchContent_MakeAvailable) to a common preamble before
test definitions, and remove the duplicate declarations (or if you truly need
two versions, rename the second content identifier to a different name like
"googletest_bench" and update usages accordingly so both can be fetched
separately).
sdk/runanywhere-commons/src/features/wakeword/wakeword_service.cpp (1)

375-384: ⚠️ Potential issue | 🟠 Major

rac_wakeword_unload_all() skips provider-side teardown.

Single-model unload forwards to provider->unload_model(), but this path only clears service->models. That leaves the backend holding live model state while the service thinks everything is gone.

Suggested fix
 RAC_API rac_result_t rac_wakeword_unload_all(rac_handle_t handle) {
     if (!is_valid_handle(handle)) {
         return RAC_ERROR_INVALID_HANDLE;
     }

     auto* service = get_service(handle);
     std::lock_guard<std::mutex> lock(service->mutex);

+    const auto* provider = service->provider;
+    if (provider && provider->unload_model && service->backend_handle) {
+        for (const auto& model : service->models) {
+            (void)provider->unload_model(service->backend_handle,
+                                         model.model_id.c_str(),
+                                         provider->user_data);
+        }
+    }
+
     service->models.clear();
     RAC_LOG_INFO("WakeWord", "Unloaded all models");

     return RAC_SUCCESS;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/features/wakeword/wakeword_service.cpp` around
lines 375 - 384, rac_wakeword_unload_all currently only clears service->models
and does not call the provider teardown, leaving backend models loaded; update
rac_wakeword_unload_all to iterate the existing entries in service->models (use
the same model representation used by single-model unload), call the provider's
unload_model(...) for each model (as done by the single-model path),
handle/provider-return errors or exceptions appropriately (log per-model
failures with RAC_LOG_*), and only after successful/unattempted unloads clear
service->models and log "Unloaded all models"; use get_service(handle) and
service->mutex for thread-safety as currently implemented.
🧹 Nitpick comments (9)
sdk/runanywhere-commons/src/infrastructure/download/download_manager.cpp (1)

64-68: Update the concurrency comment to reference the correct API name.

The comment cites rac_download_manager_start_download, but this file exposes rac_download_manager_start, which makes the rationale look stale.

✏️ Suggested comment-only cleanup
-    // path (e.g. rac_download_manager_start_download consults is_paused
+    // path (e.g. rac_download_manager_start consults is_paused
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/infrastructure/download/download_manager.cpp`
around lines 64 - 68, The concurrency comment references a stale API name;
update the comment to mention the actual public entry point
rac_download_manager_start (instead of rac_download_manager_start_download) and
keep the rest of the rationale intact: explain that flags like is_paused are
read without holding the mutex on the hot path, may be written by
pause/resume/set-healthy entry points on any thread, and therefore MUST be
std::atomic to avoid torn reads and TSan data races.
sdk/runanywhere-commons/src/features/rag/CMakeLists.txt (1)

88-92: Minor documentation inconsistency in comment.

The comment mentions onnx_embedding_provider.cpp as a user of internal/rac_platform_compat.h, but per lines 68-71, that file is compiled into rac_backend_onnx, not this rac_backend_rag target. Consider updating the comment to reference the actual RAG sources that need the internal include path, or simply state it's for general internal header access.

The CMake change itself is correct — PRIVATE visibility ensures internal headers don't leak to consumers.

📝 Suggested comment fix
-# Internal headers (e.g. internal/rac_platform_compat.h used by
-# onnx_embedding_provider.cpp). PRIVATE - never leaks to consumers.
+# Internal headers (e.g. internal/rac_platform_compat.h).
+# PRIVATE - never leaks to consumers.
 target_include_directories(rac_backend_rag PRIVATE
     ${CMAKE_SOURCE_DIR}/src
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/features/rag/CMakeLists.txt` around lines 88 -
92, The comment above the target_include_directories(rac_backend_rag PRIVATE
...) incorrectly cites onnx_embedding_provider.cpp as a consumer of
internal/rac_platform_compat.h (that file is compiled into rac_backend_onnx);
update the comment to either reference the actual RAG sources that require the
internal headers for rac_backend_rag or make it a generic note such as "Internal
headers (e.g. internal/*.h) used by RAG sources; PRIVATE - never leaks to
consumers" so the comment matches the target_include_directories and the
rac_backend_rag target.
sdk/runanywhere-commons/src/backends/onnx/rac_backend_onnx_register.cpp (2)

582-583: Consider extracting expected version as named constants at file scope.

The expected major/minor are constexpr within the block scope, which is fine. However, for easier maintenance when Sherpa-ONNX is upgraded, consider defining these at namespace scope (alongside LOG_CAT) with a comment referencing the VERSIONS file:

📝 Suggested refactor
 namespace {
 
 const char* LOG_CAT = "ONNX";
+
+// Sherpa-ONNX ABI version this code was compiled against.
+// Update when upgrading sherpa-onnx in VERSIONS file.
+constexpr int SHERPA_ONNX_EXPECTED_MAJOR = 1;
+constexpr int SHERPA_ONNX_EXPECTED_MINOR = 12;

Then reference these in the version check block.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/backends/onnx/rac_backend_onnx_register.cpp`
around lines 582 - 583, The expected_major and expected_minor constants are
defined inside a local scope; move them to file/namespace scope (near LOG_CAT)
as named constexpr constants (e.g., kExpectedSherpaOnnxMajor,
kExpectedSherpaOnnxMinor) with a comment referencing the VERSIONS file so
upgrades are obvious, then update the version check in
rac_backend_onnx_register.cpp to use these file-scope constants instead of the
local expected_major/expected_minor variables.

684-684: Consider checking return value of rac_backend_onnx_embeddings_register().

The call to rac_backend_onnx_embeddings_register() ignores its return value. Given this PR adds RAC_NODISCARD to that function, the ONNX backend should either check the result or explicitly cast to void if embeddings registration failure is acceptable.

📝 Suggested fix
     // Register ONNX embeddings provider (for RAG pipeline).
-    rac_backend_onnx_embeddings_register();
+    // Embeddings registration failure is non-fatal; RAG pipeline may still
+    // work with alternative embedding providers.
+    (void)rac_backend_onnx_embeddings_register();

Or if embeddings are required:

-    rac_backend_onnx_embeddings_register();
+    result = rac_backend_onnx_embeddings_register();
+    if (result != RAC_SUCCESS && result != RAC_ERROR_MODULE_ALREADY_REGISTERED) {
+        RAC_LOG_WARNING(LOG_CAT, "ONNX embeddings registration failed: %d", result);
+        // Continue - embeddings are optional for core STT/TTS/VAD
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/backends/onnx/rac_backend_onnx_register.cpp` at
line 684, The call to rac_backend_onnx_embeddings_register() currently ignores
its return value and must respect its RAC_NODISCARD contract; update the caller
(where rac_backend_onnx_embeddings_register() is invoked) to either check its
return value and handle failure (e.g., log the error and propagate/return a
non-success status from rac_backend_onnx_register() or the surrounding init
function) or, if embedding registration failures are acceptable, explicitly cast
the call to void ((void)rac_backend_onnx_embeddings_register()) to document
intentional ignoring; reference rac_backend_onnx_embeddings_register() and the
surrounding registration/init function when applying the change.
sdk/runanywhere-commons/include/rac/infrastructure/model_management/rac_model_assignment.h (1)

76-77: Consider adding RAC_NODISCARD to rac_model_assignment_set_callbacks for consistency.

This function returns rac_result_t but lacks RAC_NODISCARD, unlike the other functions in this header. If the PR goal is comprehensive RAC_NODISCARD coverage for all rac_result_t-returning public APIs, this appears to be an oversight.

Proposed fix
-RAC_API rac_result_t
+RAC_API RAC_NODISCARD rac_result_t
 rac_model_assignment_set_callbacks(const rac_assignment_callbacks_t* callbacks);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-commons/include/rac/infrastructure/model_management/rac_model_assignment.h`
around lines 76 - 77, Add the RAC_NODISCARD attribute to the
rac_model_assignment_set_callbacks declaration so its rac_result_t return is
annotated consistently with the other public APIs; edit the declaration of
rac_model_assignment_set_callbacks to match the pattern used elsewhere (place
RAC_NODISCARD in the same position relative to RAC_API/rac_result_t as other
functions) and run a quick build to ensure the macro placement compiles cleanly.
sdk/runanywhere-commons/include/rac/features/llm/rac_llm_structured_output.h (1)

126-128: Consider adding RAC_NODISCARD to rac_structured_output_validate for consistency.

This function returns rac_result_t but lacks RAC_NODISCARD, unlike the other rac_result_t-returning functions in this header. If the PR goal is comprehensive RAC_NODISCARD coverage, this appears to be an oversight.

Proposed fix
-RAC_API rac_result_t
+RAC_API RAC_NODISCARD rac_result_t
 rac_structured_output_validate(const char* text, const rac_structured_output_config_t* config,
                                rac_structured_output_validation_t* out_validation);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/include/rac/features/llm/rac_llm_structured_output.h`
around lines 126 - 128, The declaration for rac_structured_output_validate is
missing the RAC_NODISCARD attribute like other rac_result_t-returning functions;
update its declaration to prepend RAC_NODISCARD so callers are warned if the
return value is ignored (modify the function prototype for
rac_structured_output_validate in rac_llm_structured_output.h to include
RAC_NODISCARD and keep the existing signature and parameter names).
sdk/runanywhere-commons/src/jni/jni_shared.h (1)

55-69: Consider using the RAC logger API for the non-Android fallback.

The non-Android fallback macros (lines 65-68) use raw fprintf. Per coding guidelines, logging should use the RAC logger API (RAC_LOG_INFO, etc.). While the Android path uses __android_log_print for logcat integration, the desktop fallback could route through the RAC logger for consistency.

That said, since this is internal JNI scaffolding and the macros already exist in runanywhere_commons_jni.cpp, maintaining consistency with the existing codebase is reasonable.

Optional: Route non-Android logging through RAC logger
 `#else`
 `#include` <cstdio>
-#define LOGi(...) do { std::fprintf(stdout, "[INFO]  " __VA_ARGS__); std::fputc('\n', stdout); } while (0)
-#define LOGd(...) do { std::fprintf(stdout, "[DEBUG] " __VA_ARGS__); std::fputc('\n', stdout); } while (0)
-#define LOGw(...) do { std::fprintf(stderr, "[WARN]  " __VA_ARGS__); std::fputc('\n', stderr); } while (0)
-#define LOGe(...) do { std::fprintf(stderr, "[ERROR] " __VA_ARGS__); std::fputc('\n', stderr); } while (0)
+#define LOGi(...) RAC_LOG_INFO(__VA_ARGS__)
+#define LOGd(...) RAC_LOG_DEBUG(__VA_ARGS__)
+#define LOGw(...) RAC_LOG_WARNING(__VA_ARGS__)
+#define LOGe(...) RAC_LOG_ERROR(__VA_ARGS__)
 `#endif`

As per coding guidelines: "Logging must use the RAC logger API (RAC_LOG_INFO, RAC_LOG_ERROR, RAC_LOG_WARNING, RAC_LOG_DEBUG) via rac/core/rac_logger.h"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/jni/jni_shared.h` around lines 55 - 69, Replace
the non-Android logging macros in jni_shared.h (LOGi, LOGd, LOGw, LOGe) to call
the RAC logger API (RAC_LOG_INFO, RAC_LOG_DEBUG, RAC_LOG_WARNING, RAC_LOG_ERROR)
instead of using fprintf; add an `#include` for rac/core/rac_logger.h inside the
non-Android branch, keep the Android __android_log_print path unchanged, and
preserve the variadic macro usage so existing calls to LOGi/LOGd/LOGw/LOGe
continue to work with the RAC logger.
sdk/runanywhere-commons/tests/test_core.cpp (1)

131-147: Add the same post-failure init-state assertion here for symmetry.

test_adapter_version_future_rejected should also assert rac_is_initialized() == RAC_FALSE, matching the zero-version test and guarding against partial-init regressions.

💡 Suggested test hardening
 static TestResult test_adapter_version_future_rejected() {
@@
     rac_result_t rc = rac_init(&config);
     ASSERT_EQ(rc, RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION,
               "rac_init with future version should return "
               "RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION");
+    ASSERT_EQ(rac_is_initialized(), RAC_FALSE,
+              "init failure must not leave SDK in initialized state");
     return TEST_PASS();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/tests/test_core.cpp` around lines 131 - 147, Add a
post-failure initialization-state assertion to
test_adapter_version_future_rejected: after the rac_init(&config) ASSERT_EQ(...)
check, add an assertion that rac_is_initialized() == RAC_FALSE to mirror the
zero-version test and ensure no partial init occurred; locate this in the
test_adapter_version_future_rejected function alongside the existing
rac_init/rac_result_t rc check and use the same ASSERT_EQ style and messaging as
the other test for consistency.
sdk/runanywhere-commons/include/rac/features/wakeword/rac_wakeword_service.h (1)

423-423: Missing RAC_NODISCARD for consistency.

rac_wakeword_provider_set returns rac_result_t but lacks RAC_NODISCARD, unlike all other rac_result_t-returning functions in this file. Even though the doc says it "always returns RAC_SUCCESS", future versions might add validation that could fail.

♻️ Suggested fix
-RAC_API rac_result_t rac_wakeword_provider_set(const rac_wakeword_provider_ops_t* ops);
+RAC_API RAC_NODISCARD rac_result_t rac_wakeword_provider_set(const rac_wakeword_provider_ops_t* ops);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/include/rac/features/wakeword/rac_wakeword_service.h`
at line 423, Add the missing RAC_NODISCARD attribute to the declaration of
rac_wakeword_provider_set to match other rac_result_t-returning functions and
ensure callers don't ignore the return value; locate the function declaration
for rac_wakeword_provider_set (the prototype returning rac_result_t) in
rac_wakeword_service.h and prepend or insert RAC_NODISCARD so the signature
mirrors the other annotated functions in the file.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@sdk/runanywhere-commons/cmake/Sanitizers.cmake`:
- Around line 130-140: The ENABLE_MSAN block in Sanitizers.cmake currently only
checks CMAKE_CXX_COMPILER_ID for Clang but must also verify the target platform;
update the ENABLE_MSAN handling to reject non-supported OSes by checking
CMAKE_SYSTEM_NAME (or equivalent) and erroring out unless it matches Linux (or
allowed BSDs like FreeBSD/NetBSD), so you fail at configure time instead of
applying MSan flags on macOS; keep the existing Clang check and the list(APPEND
_san_cflags/_san_lflags) behavior but add a platform guard that calls
message(FATAL_ERROR "MemorySanitizer requires Linux (or FreeBSD/NetBSD).") when
the OS is unsupported.

In `@sdk/runanywhere-commons/CMakePresets.json`:
- Around line 84-87: Add explicit CC/CXX compiler environment variables to the
dev presets so builds are pinned to gcc/g++; for the existing presets dev-asan,
dev-ubsan, and dev-tsan, update their "environment" objects to include "CC":
"gcc" and "CXX": "g++"; for dev-no-backends, create an "environment" section and
add the same "CC" and "CXX" entries so all four dev presets consistently use
gcc/g++.

In `@sdk/runanywhere-commons/CPP_LAYER_CLEANUP_STATUS.md`:
- Line 3: Update the hard-coded commit count in the document header: replace the
exact line containing "Branch: `smonga/cpp-layer-full-cleanup` (12 commits,
branched from `main @ bc7db9bd0`)" so it no longer hard-codes "12 commits" —
either remove the numeric count entirely or change it to a non-stale form like
"N+ commits" (or "multiple commits") so the header stays accurate as the table
below changes.

In `@sdk/runanywhere-commons/include/rac/core/rac_error.h`:
- Around line 370-380: Add mappings for the two new error macros inside the
rac_error_message() switch so callers get descriptive messages: locate
rac_error_message (in sdk/runanywhere-commons/src/core/rac_error.cpp) and add
case RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION to return a string describing the
backend runtime ABI/version mismatch and case RAC_ERROR_JNI_EXCEPTION to return
a string describing a pending Java exception from a JNI callback; ensure the
returned messages match the comments in rac_error.h and follow the existing
formatting/style of other case branches.

In `@sdk/runanywhere-commons/include/rac/core/rac_platform_adapter.h`:
- Around line 95-98: The comment references a nonexistent macro
`RAC_PLATFORM_ADAPTER_VERSION_LATEST`; update the comment to match the actual
symbol names used in this header by either changing the text to only mention
`RAC_PLATFORM_ADAPTER_VERSION` (and explain bumping that macro when callbacks
are appended) or add/introduce a proper `RAC_PLATFORM_ADAPTER_VERSION_LATEST`
definition if intended; locate the comment near `RAC_PLATFORM_ADAPTER_VERSION`
and make the wording consistent with the defined macro so consumers can rely on
the documented symbol.

In `@sdk/runanywhere-commons/src/backends/onnx/wakeword_onnx.cpp`:
- Around line 1038-1057: The provider creation path in
onnx_wakeword_provider_create currently only populates
rac_wakeword_onnx_config_t and calls rac_wakeword_onnx_create, but never
initializes the shared melspec/embedding sessions so detection can silently be
disabled; update onnx_wakeword_provider_create to call
rac_wakeword_onnx_init_shared_models (or the appropriate shared-model init
function) before rac_wakeword_onnx_create and propagate any error (return
non-success) if shared-model initialization fails so the provider fails fast
when sessions are unavailable, or alternatively check post-create that the
shared sessions exist and return an error instead of silently succeeding.
- Around line 1127-1134: The code currently ignores the return value of
rac_wakeword_provider_set(&g_onnx_wakeword_provider_ops) then unconditionally
sets g_wakeword_onnx_registered = true and logs success; change this so you
check the provider_set return (and similarly the provider_clear return in the
corresponding clear block), only set g_wakeword_onnx_registered = true after a
successful rac_wakeword_provider_set, and on failure log a descriptive error via
RAC_LOG_ERROR(LOG_TAG, ...) including the return code and return an error status
(do not flip the registered flag). Also mirror this behavior for the
provider_clear path: call rac_wakeword_provider_clear, check its return, only
clear g_wakeword_onnx_registered on success, and log/return on failure.

In `@sdk/runanywhere-commons/src/core/rac_core.cpp`:
- Around line 113-117: The error detail string passed to rac_error_set_details
contains an unexpanded template token "{0}-init"; update the message in
rac_error_set_details (in rac_core.cpp) to remove the placeholder and use a
concrete reference such as "the adapter initializer (e.g., rac_init)" or "your
adapter initialization function" so callers are not confused; keep the rest of
the text intact and ensure the message references rac_init and/or the platform
adapter .version assignment explicitly.

In `@sdk/runanywhere-commons/src/core/rac_hardware.cpp`:
- Around line 116-125: kHasANE is currently set to true for all Apple targets;
restrict it to real Apple Silicon devices only and exclude simulator builds by
changing the preprocessor condition around kHasANE (and including
TargetConditionals.h if needed). Replace the current defined(__APPLE__) branch
with a condition like defined(__APPLE__) && (defined(__aarch64__) ||
defined(__arm64__)) && !TARGET_OS_SIMULATOR (or
!defined(__ENVIRONMENT_IPHONE_SIMULATOR__) / using TARGET_OS_SIMULATOR from
TargetConditionals.h) so kHasANE is RAC_TRUE only on actual Apple Silicon
devices and RAC_FALSE elsewhere.

In `@sdk/runanywhere-commons/src/features/wakeword/wakeword_service.cpp`:
- Around line 39-41: The service currently uses the global g_provider at init
but re-reads it for later ops, which can misroute old backend_handle through a
new vtable; modify WakewordService to capture and store the provider pointer
(const rac_wakeword_provider_ops_t*) used when creating backend_handle and use
that stored ops table for all lifecycle calls (destroy, process, unload,
set_threshold) instead of reading g_provider each time; alternatively, implement
a guard in rac_wakeword_provider_set() to reject provider changes while
WakewordService instances exist—update methods referencing g_provider to use the
instance field (and ensure constructor stores the provider) so cleanup always
uses the correct ops.

In `@sdk/runanywhere-commons/src/jni/jni_scope.h`:
- Line 56: The backend JNI targets rac_backend_onnx_jni,
rac_backend_whispercpp_jni, rac_backend_llamacpp_jni, and rac_backend_rag_jni
are missing ${JNI_INCLUDE_DIRS} in their target_include_directories calls;
update each CMakeLists.txt for these targets to add ${JNI_INCLUDE_DIRS} (as
SYSTEM or PUBLIC/PRIVATE as appropriate) to the target_include_directories()
invocation so that <jni.h> is available for Android builds (ensure you reference
the exact target names above when making the change).

In `@sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp`:
- Around line 2859-2864: In jni_device_get_info handling, guard against
GetStringUTFChars returning null: after obtaining jstring jResult from
cb_result.get() and before calling env->GetStringUTFChars (or using json_str),
check that jResult is non-null and that the returned const char* from
GetStringUTFChars is not null; if either is null, call RAC_JNI_TRY_VOID(s) or
return early (releasing resources appropriately) instead of proceeding to log or
json::parse; update the code paths around rac::jni::JniScope s(...), cb_result,
g_device_jni_state.get_device_info_method, and any subsequent json::parse and
env->ReleaseStringUTFChars calls to only run when the UTF chars pointer is valid
so we avoid UB on OOM/JNI failures.
- Around line 807-830: The code currently ignores Java exceptions cleared by
rac::jni::JniScope (js.failed() / hadException) and continues generation
returning RAC_TRUE; update the token-callback handling in
racLlmComponentGenerateStreamWithCallback's loop so that if js.failed() (i.e.,
hadException is true) after calling ctx->onTokenMethod on ctx->callback (both
byte and string paths) you mark the context as failed and return RAC_FALSE
immediately; ensure you reference JniScope js, ctx->onTokenExpectsBytes,
ctx->callback and ctx->onTokenMethod and replace the current unconditional
RAC_TRUE return with a failure path that stops the stream.

In `@sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_error.h`:
- Around line 368-377: The switch in rac_error_message() is missing case
mappings for RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION and RAC_ERROR_JNI_EXCEPTION,
so callers get "Unknown error code"; add two cases in the BACKEND ERRORS section
of rac_error.cpp within rac_error_message(): one for
RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION that returns "Backend ABI incompatible
with SDK version" and one for RAC_ERROR_JNI_EXCEPTION that returns "JNI
exception occurred".

---

Outside diff comments:
In `@sdk/runanywhere-commons/src/features/wakeword/wakeword_service.cpp`:
- Around line 375-384: rac_wakeword_unload_all currently only clears
service->models and does not call the provider teardown, leaving backend models
loaded; update rac_wakeword_unload_all to iterate the existing entries in
service->models (use the same model representation used by single-model unload),
call the provider's unload_model(...) for each model (as done by the
single-model path), handle/provider-return errors or exceptions appropriately
(log per-model failures with RAC_LOG_*), and only after successful/unattempted
unloads clear service->models and log "Unloaded all models"; use
get_service(handle) and service->mutex for thread-safety as currently
implemented.

In `@sdk/runanywhere-commons/tests/CMakeLists.txt`:
- Around line 275-287: There are two FetchContent_Declare blocks for the same
content name "googletest" with different GIT_TAGs (release-1.12.1 and v1.14.0),
so CMake will honor the first and ignore the second; to fix, consolidate to a
single FetchContent_Declare for googletest with the desired version (pick one
GIT_TAG, e.g., v1.14.0), move that single declaration (and
FetchContent_MakeAvailable) to a common preamble before test definitions, and
remove the duplicate declarations (or if you truly need two versions, rename the
second content identifier to a different name like "googletest_bench" and update
usages accordingly so both can be fetched separately).

---

Nitpick comments:
In
`@sdk/runanywhere-commons/include/rac/features/llm/rac_llm_structured_output.h`:
- Around line 126-128: The declaration for rac_structured_output_validate is
missing the RAC_NODISCARD attribute like other rac_result_t-returning functions;
update its declaration to prepend RAC_NODISCARD so callers are warned if the
return value is ignored (modify the function prototype for
rac_structured_output_validate in rac_llm_structured_output.h to include
RAC_NODISCARD and keep the existing signature and parameter names).

In
`@sdk/runanywhere-commons/include/rac/features/wakeword/rac_wakeword_service.h`:
- Line 423: Add the missing RAC_NODISCARD attribute to the declaration of
rac_wakeword_provider_set to match other rac_result_t-returning functions and
ensure callers don't ignore the return value; locate the function declaration
for rac_wakeword_provider_set (the prototype returning rac_result_t) in
rac_wakeword_service.h and prepend or insert RAC_NODISCARD so the signature
mirrors the other annotated functions in the file.

In
`@sdk/runanywhere-commons/include/rac/infrastructure/model_management/rac_model_assignment.h`:
- Around line 76-77: Add the RAC_NODISCARD attribute to the
rac_model_assignment_set_callbacks declaration so its rac_result_t return is
annotated consistently with the other public APIs; edit the declaration of
rac_model_assignment_set_callbacks to match the pattern used elsewhere (place
RAC_NODISCARD in the same position relative to RAC_API/rac_result_t as other
functions) and run a quick build to ensure the macro placement compiles cleanly.

In `@sdk/runanywhere-commons/src/backends/onnx/rac_backend_onnx_register.cpp`:
- Around line 582-583: The expected_major and expected_minor constants are
defined inside a local scope; move them to file/namespace scope (near LOG_CAT)
as named constexpr constants (e.g., kExpectedSherpaOnnxMajor,
kExpectedSherpaOnnxMinor) with a comment referencing the VERSIONS file so
upgrades are obvious, then update the version check in
rac_backend_onnx_register.cpp to use these file-scope constants instead of the
local expected_major/expected_minor variables.
- Line 684: The call to rac_backend_onnx_embeddings_register() currently ignores
its return value and must respect its RAC_NODISCARD contract; update the caller
(where rac_backend_onnx_embeddings_register() is invoked) to either check its
return value and handle failure (e.g., log the error and propagate/return a
non-success status from rac_backend_onnx_register() or the surrounding init
function) or, if embedding registration failures are acceptable, explicitly cast
the call to void ((void)rac_backend_onnx_embeddings_register()) to document
intentional ignoring; reference rac_backend_onnx_embeddings_register() and the
surrounding registration/init function when applying the change.

In `@sdk/runanywhere-commons/src/features/rag/CMakeLists.txt`:
- Around line 88-92: The comment above the
target_include_directories(rac_backend_rag PRIVATE ...) incorrectly cites
onnx_embedding_provider.cpp as a consumer of internal/rac_platform_compat.h
(that file is compiled into rac_backend_onnx); update the comment to either
reference the actual RAG sources that require the internal headers for
rac_backend_rag or make it a generic note such as "Internal headers (e.g.
internal/*.h) used by RAG sources; PRIVATE - never leaks to consumers" so the
comment matches the target_include_directories and the rac_backend_rag target.

In `@sdk/runanywhere-commons/src/infrastructure/download/download_manager.cpp`:
- Around line 64-68: The concurrency comment references a stale API name; update
the comment to mention the actual public entry point rac_download_manager_start
(instead of rac_download_manager_start_download) and keep the rest of the
rationale intact: explain that flags like is_paused are read without holding the
mutex on the hot path, may be written by pause/resume/set-healthy entry points
on any thread, and therefore MUST be std::atomic to avoid torn reads and TSan
data races.

In `@sdk/runanywhere-commons/src/jni/jni_shared.h`:
- Around line 55-69: Replace the non-Android logging macros in jni_shared.h
(LOGi, LOGd, LOGw, LOGe) to call the RAC logger API (RAC_LOG_INFO,
RAC_LOG_DEBUG, RAC_LOG_WARNING, RAC_LOG_ERROR) instead of using fprintf; add an
`#include` for rac/core/rac_logger.h inside the non-Android branch, keep the
Android __android_log_print path unchanged, and preserve the variadic macro
usage so existing calls to LOGi/LOGd/LOGw/LOGe continue to work with the RAC
logger.

In `@sdk/runanywhere-commons/tests/test_core.cpp`:
- Around line 131-147: Add a post-failure initialization-state assertion to
test_adapter_version_future_rejected: after the rac_init(&config) ASSERT_EQ(...)
check, add an assertion that rac_is_initialized() == RAC_FALSE to mirror the
zero-version test and ensure no partial init occurred; locate this in the
test_adapter_version_future_rejected function alongside the existing
rac_init/rac_result_t rc check and use the same ASSERT_EQ style and messaging as
the other test for consistency.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e04b9299-f308-4c83-af16-d156753bdeaf

📥 Commits

Reviewing files that changed from the base of the PR and between bc7db9b and 459c324.

📒 Files selected for processing (104)
  • sdk/runanywhere-commons/.clang-tidy
  • sdk/runanywhere-commons/CMakeLists.txt
  • sdk/runanywhere-commons/CMakePresets.json
  • sdk/runanywhere-commons/CPP_LAYER_CLEANUP_STATUS.md
  • sdk/runanywhere-commons/cmake/Sanitizers.cmake
  • sdk/runanywhere-commons/include/rac/backends/rac_embeddings_onnx.h
  • sdk/runanywhere-commons/include/rac/backends/rac_stt_whisperkit_coreml.h
  • sdk/runanywhere-commons/include/rac/core/capabilities/rac_lifecycle.h
  • sdk/runanywhere-commons/include/rac/core/rac_analytics_events.h
  • sdk/runanywhere-commons/include/rac/core/rac_attrs.h
  • sdk/runanywhere-commons/include/rac/core/rac_audio_utils.h
  • sdk/runanywhere-commons/include/rac/core/rac_benchmark_log.h
  • sdk/runanywhere-commons/include/rac/core/rac_benchmark_stats.h
  • sdk/runanywhere-commons/include/rac/core/rac_core.h
  • sdk/runanywhere-commons/include/rac/core/rac_error.h
  • sdk/runanywhere-commons/include/rac/core/rac_events.h
  • sdk/runanywhere-commons/include/rac/core/rac_hardware.h
  • sdk/runanywhere-commons/include/rac/core/rac_logger.h
  • sdk/runanywhere-commons/include/rac/core/rac_platform_adapter.h
  • sdk/runanywhere-commons/include/rac/core/rac_sdk_state.h
  • sdk/runanywhere-commons/include/rac/core/rac_structured_error.h
  • sdk/runanywhere-commons/include/rac/core/rac_types.h
  • sdk/runanywhere-commons/include/rac/features/diffusion/rac_diffusion_component.h
  • sdk/runanywhere-commons/include/rac/features/diffusion/rac_diffusion_model_registry.h
  • sdk/runanywhere-commons/include/rac/features/diffusion/rac_diffusion_service.h
  • sdk/runanywhere-commons/include/rac/features/diffusion/rac_diffusion_tokenizer.h
  • sdk/runanywhere-commons/include/rac/features/embeddings/rac_embeddings_component.h
  • sdk/runanywhere-commons/include/rac/features/embeddings/rac_embeddings_service.h
  • sdk/runanywhere-commons/include/rac/features/llm/rac_llm_analytics.h
  • sdk/runanywhere-commons/include/rac/features/llm/rac_llm_component.h
  • sdk/runanywhere-commons/include/rac/features/llm/rac_llm_events.h
  • sdk/runanywhere-commons/include/rac/features/llm/rac_llm_metrics.h
  • sdk/runanywhere-commons/include/rac/features/llm/rac_llm_service.h
  • sdk/runanywhere-commons/include/rac/features/llm/rac_llm_structured_output.h
  • sdk/runanywhere-commons/include/rac/features/llm/rac_tool_calling.h
  • sdk/runanywhere-commons/include/rac/features/platform/rac_diffusion_platform.h
  • sdk/runanywhere-commons/include/rac/features/platform/rac_llm_platform.h
  • sdk/runanywhere-commons/include/rac/features/platform/rac_tts_platform.h
  • sdk/runanywhere-commons/include/rac/features/rag/rac_rag.h
  • sdk/runanywhere-commons/include/rac/features/rag/rac_rag_pipeline.h
  • sdk/runanywhere-commons/include/rac/features/stt/rac_stt_analytics.h
  • sdk/runanywhere-commons/include/rac/features/stt/rac_stt_component.h
  • sdk/runanywhere-commons/include/rac/features/stt/rac_stt_events.h
  • sdk/runanywhere-commons/include/rac/features/stt/rac_stt_service.h
  • sdk/runanywhere-commons/include/rac/features/tts/rac_tts_analytics.h
  • sdk/runanywhere-commons/include/rac/features/tts/rac_tts_component.h
  • sdk/runanywhere-commons/include/rac/features/tts/rac_tts_events.h
  • sdk/runanywhere-commons/include/rac/features/tts/rac_tts_service.h
  • sdk/runanywhere-commons/include/rac/features/vad/rac_vad_analytics.h
  • sdk/runanywhere-commons/include/rac/features/vad/rac_vad_component.h
  • sdk/runanywhere-commons/include/rac/features/vad/rac_vad_energy.h
  • sdk/runanywhere-commons/include/rac/features/vad/rac_vad_events.h
  • sdk/runanywhere-commons/include/rac/features/vad/rac_vad_service.h
  • sdk/runanywhere-commons/include/rac/features/vlm/rac_vlm_component.h
  • sdk/runanywhere-commons/include/rac/features/vlm/rac_vlm_service.h
  • sdk/runanywhere-commons/include/rac/features/voice_agent/rac_voice_agent.h
  • sdk/runanywhere-commons/include/rac/features/wakeword/rac_wakeword_service.h
  • sdk/runanywhere-commons/include/rac/infrastructure/device/rac_device_manager.h
  • sdk/runanywhere-commons/include/rac/infrastructure/download/rac_download.h
  • sdk/runanywhere-commons/include/rac/infrastructure/download/rac_download_orchestrator.h
  • sdk/runanywhere-commons/include/rac/infrastructure/events/rac_events.h
  • sdk/runanywhere-commons/include/rac/infrastructure/extraction/rac_extraction.h
  • sdk/runanywhere-commons/include/rac/infrastructure/file_management/rac_file_manager.h
  • sdk/runanywhere-commons/include/rac/infrastructure/model_management/rac_lora_registry.h
  • sdk/runanywhere-commons/include/rac/infrastructure/model_management/rac_model_assignment.h
  • sdk/runanywhere-commons/include/rac/infrastructure/model_management/rac_model_compatibility.h
  • sdk/runanywhere-commons/include/rac/infrastructure/model_management/rac_model_paths.h
  • sdk/runanywhere-commons/include/rac/infrastructure/model_management/rac_model_registry.h
  • sdk/runanywhere-commons/include/rac/infrastructure/model_management/rac_model_strategy.h
  • sdk/runanywhere-commons/include/rac/infrastructure/model_management/rac_model_types.h
  • sdk/runanywhere-commons/include/rac/infrastructure/storage/rac_storage_analyzer.h
  • sdk/runanywhere-commons/include/rac/infrastructure/telemetry/rac_telemetry_manager.h
  • sdk/runanywhere-commons/include/rac/server/rac_server.h
  • sdk/runanywhere-commons/include/rac/utils/rac_image_utils.h
  • sdk/runanywhere-commons/src/backends/backend_interface.h
  • sdk/runanywhere-commons/src/backends/onnx/CMakeLists.txt
  • sdk/runanywhere-commons/src/backends/onnx/onnx_backend.cpp
  • sdk/runanywhere-commons/src/backends/onnx/rac_backend_onnx_register.cpp
  • sdk/runanywhere-commons/src/backends/onnx/wakeword_onnx.cpp
  • sdk/runanywhere-commons/src/core/rac_core.cpp
  • sdk/runanywhere-commons/src/core/rac_hardware.cpp
  • sdk/runanywhere-commons/src/features/rag/CMakeLists.txt
  • sdk/runanywhere-commons/src/features/rag/onnx_embedding_provider.cpp
  • sdk/runanywhere-commons/src/features/vlm/vlm_component.cpp
  • sdk/runanywhere-commons/src/features/wakeword/wakeword_service.cpp
  • sdk/runanywhere-commons/src/infrastructure/download/download_manager.cpp
  • sdk/runanywhere-commons/src/infrastructure/download/download_orchestrator.cpp
  • sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp
  • sdk/runanywhere-commons/src/internal/rac_platform_compat.h
  • sdk/runanywhere-commons/src/jni/jni_scope.h
  • sdk/runanywhere-commons/src/jni/jni_shared.h
  • sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp
  • sdk/runanywhere-commons/tests/CMakeLists.txt
  • sdk/runanywhere-commons/tests/test_config.h
  • sdk/runanywhere-commons/tests/test_core.cpp
  • sdk/runanywhere-commons/tests/test_download_orchestrator.cpp
  • sdk/runanywhere-commons/tests/test_extraction.cpp
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_platform.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/ffi_types.dart
  • sdk/runanywhere-react-native/packages/core/cpp/bridges/InitBridge.cpp
  • sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_error.h
  • sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_platform_adapter.h
  • sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+PlatformAdapter.swift
  • sdk/runanywhere-web/packages/llamacpp/src/Foundation/PlatformAdapter.ts

Comment on lines +130 to +140
if(ENABLE_MSAN)
# MSan requires Clang + Linux and all linked libs to be MSan-built.
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang")
message(FATAL_ERROR "MemorySanitizer requires Clang.")
endif()
list(APPEND _san_cflags
-fsanitize=memory
-fsanitize-memory-track-origins=2
-fno-omit-frame-pointer -g)
list(APPEND _san_lflags -fsanitize=memory)
message(STATUS "[${target}] MemorySanitizer: ENABLED")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does LLVM/Clang MemorySanitizer support macOS, or is it limited to Linux targets?

💡 Result:

LLVM/Clang MemorySanitizer does not support macOS. It is limited to Linux, FreeBSD, and NetBSD targets.

Citations:


Add platform check to reject ENABLE_MSAN on non-Linux systems.

The implementation checks only for Clang but the documented constraint is "MSan requires Clang + Linux". On macOS with AppleClang, the MSan flags will be applied and fail during the build instead of at configure time. LLVM/Clang MemorySanitizer is limited to Linux (and FreeBSD/NetBSD), not supported on macOS.

Suggested fix
     if(ENABLE_MSAN)
         # MSan requires Clang + Linux and all linked libs to be MSan-built.
+        if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")
+            message(FATAL_ERROR "MemorySanitizer is supported only on Linux.")
+        endif()
         if(NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang")
             message(FATAL_ERROR "MemorySanitizer requires Clang.")
         endif()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if(ENABLE_MSAN)
# MSan requires Clang + Linux and all linked libs to be MSan-built.
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang")
message(FATAL_ERROR "MemorySanitizer requires Clang.")
endif()
list(APPEND _san_cflags
-fsanitize=memory
-fsanitize-memory-track-origins=2
-fno-omit-frame-pointer -g)
list(APPEND _san_lflags -fsanitize=memory)
message(STATUS "[${target}] MemorySanitizer: ENABLED")
if(ENABLE_MSAN)
# MSan requires Clang + Linux and all linked libs to be MSan-built.
if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")
message(FATAL_ERROR "MemorySanitizer is supported only on Linux.")
endif()
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang")
message(FATAL_ERROR "MemorySanitizer requires Clang.")
endif()
list(APPEND _san_cflags
-fsanitize=memory
-fsanitize-memory-track-origins=2
-fno-omit-frame-pointer -g)
list(APPEND _san_lflags -fsanitize=memory)
message(STATUS "[${target}] MemorySanitizer: ENABLED")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/cmake/Sanitizers.cmake` around lines 130 - 140, The
ENABLE_MSAN block in Sanitizers.cmake currently only checks
CMAKE_CXX_COMPILER_ID for Clang but must also verify the target platform; update
the ENABLE_MSAN handling to reject non-supported OSes by checking
CMAKE_SYSTEM_NAME (or equivalent) and erroring out unless it matches Linux (or
allowed BSDs like FreeBSD/NetBSD), so you fail at configure time instead of
applying MSan flags on macOS; keep the existing Clang check and the list(APPEND
_san_cflags/_san_lflags) behavior but add a platform guard that calls
message(FATAL_ERROR "MemorySanitizer requires Linux (or FreeBSD/NetBSD).") when
the OS is unsupported.

Comment on lines +84 to +87
"environment": {
"ASAN_OPTIONS": "detect_leaks=1:abort_on_error=1:strict_string_checks=1",
"UBSAN_OPTIONS": "print_stacktrace=1:halt_on_error=1"
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether sanitizer presets pin compilers
python - <<'PY'
import json
with open("sdk/runanywhere-commons/CMakePresets.json") as f:
    data = json.load(f)
for p in data.get("configurePresets", []):
    if p.get("name", "").startswith("dev-"):
        env = p.get("environment", {})
        print(p["name"], "CC=", env.get("CC"), "CXX=", env.get("CXX"))
PY

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 193


🏁 Script executed:

#!/bin/bash
# Extract and verify the CMakePresets.json structure around the specified lines
cat -n sdk/runanywhere-commons/CMakePresets.json | sed -n '80,120p'

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 1882


🏁 Script executed:

#!/bin/bash
# Find dev-no-backends preset and its environment section
cat -n sdk/runanywhere-commons/CMakePresets.json | sed -n '50,88p'

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 1928


🏁 Script executed:

#!/bin/bash
# Find dev-no-backends preset
grep -n "dev-no-backends" sdk/runanywhere-commons/CMakePresets.json

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 109


🏁 Script executed:

#!/bin/bash
# View the dev-no-backends preset and its structure
cat -n sdk/runanywhere-commons/CMakePresets.json | sed -n '18,50p'

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 1345


Add CC/CXX compiler env vars to all dev presets.

All four dev presets lack explicit compiler configuration. Per C++ Commons guidelines, gcc/g++ must be pinned to avoid clang toolchain drift, which breaks builds on this platform due to missing C++ stdlib headers.

Update three presets with existing environment sections (dev-asan at 84-87, dev-ubsan at 100-102, dev-tsan at 115-117) by adding:

"CC": "gcc",
"CXX": "g++",

Also add an "environment" section to dev-no-backends (currently missing) with the same compiler variables.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/CMakePresets.json` around lines 84 - 87, Add explicit
CC/CXX compiler environment variables to the dev presets so builds are pinned to
gcc/g++; for the existing presets dev-asan, dev-ubsan, and dev-tsan, update
their "environment" objects to include "CC": "gcc" and "CXX": "g++"; for
dev-no-backends, create an "environment" section and add the same "CC" and "CXX"
entries so all four dev presets consistently use gcc/g++.

@@ -0,0 +1,161 @@
# C++ Layer Cleanup — Branch Status

> **Branch**: `smonga/cpp-layer-full-cleanup` (12 commits, branched from `main @ bc7db9bd0`).
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Commit-count metadata is already stale.

Line [3] says “12 commits,” but the same document’s table has more entries. Prefer removing the hard-coded count or making it “N+ commits” to avoid immediate drift.

📝 Suggested doc tweak
-> **Branch**: `smonga/cpp-layer-full-cleanup` (12 commits, branched from `main @ bc7db9bd0`).
+> **Branch**: `smonga/cpp-layer-full-cleanup` (branched from `main @ bc7db9bd0`).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
> **Branch**: `smonga/cpp-layer-full-cleanup` (12 commits, branched from `main @ bc7db9bd0`).
> **Branch**: `smonga/cpp-layer-full-cleanup` (branched from `main @ bc7db9bd0`).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/CPP_LAYER_CLEANUP_STATUS.md` at line 3, Update the
hard-coded commit count in the document header: replace the exact line
containing "Branch: `smonga/cpp-layer-full-cleanup` (12 commits, branched from
`main @ bc7db9bd0`)" so it no longer hard-codes "12 commits" — either remove the
numeric count entirely or change it to a non-stale form like "N+ commits" (or
"multiple commits") so the header stays accurate as the table below changes.

Comment on lines +370 to +380
/** Backend runtime ABI is incompatible with the version the SDK was compiled
* against. Typically means the prebuilt third_party binary (sherpa-onnx,
* ONNX Runtime, etc.) was produced from a different major/minor version
* than the headers; struct layouts may not match, so we refuse to proceed.
* Fix: align third_party binaries with the VERSIONS file entries, or
* upgrade the commons SDK. */
#define RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION ((rac_result_t) - 605)
/** JNI call from a Kotlin/Java callback left a pending Java exception on
* the JNIEnv - caught by JniScope, logged, and cleared. Returned from C
* to signal that the operation must be retried or abandoned. */
#define RAC_ERROR_JNI_EXCEPTION ((rac_result_t) - 606)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -i "rac_error.cpp" sdk/runanywhere-commons | while read -r f; do
  echo "== Inspecting: $f =="
  rg -n -C3 "rac_error_message|RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION|RAC_ERROR_JNI_EXCEPTION|-605|-606" "$f"
done

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 309


🏁 Script executed:

#!/bin/bash
# Get the full rac_error_message() implementation from rac_error.cpp
rg -n "^const char\* rac_error_message|RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION|RAC_ERROR_JNI_EXCEPTION|-605|-606" sdk/runanywhere-commons/src/core/rac_error.cpp -A 200 | head -250

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 9678


🏁 Script executed:

#!/bin/bash
# Search for -600 range error codes and their case statements
rg -n "case RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION|case RAC_ERROR_JNI_EXCEPTION|-605|-606|// =.*-6[0-9][0-9]" sdk/runanywhere-commons/src/core/rac_error.cpp

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 56


🏁 Script executed:

#!/bin/bash
# Get the entire rac_error_message() function
sed -n '22,/^}/p' sdk/runanywhere-commons/src/core/rac_error.cpp | tail -100

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 4500


Add case statements for RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION and RAC_ERROR_JNI_EXCEPTION in rac_error_message().

Both new error codes (-605 and -606) are missing from the switch statement in rac_error_message() implementation (sdk/runanywhere-commons/src/core/rac_error.cpp). Without mappings, callers will receive the generic "Unknown error code" message instead of descriptive text.

Add to BACKEND ERRORS section in rac_error_message():
case RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION:
    return "Backend runtime ABI is incompatible with the version the SDK was compiled against";
case RAC_ERROR_JNI_EXCEPTION:
    return "JNI call from a Kotlin/Java callback left a pending Java exception";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/include/rac/core/rac_error.h` around lines 370 - 380,
Add mappings for the two new error macros inside the rac_error_message() switch
so callers get descriptive messages: locate rac_error_message (in
sdk/runanywhere-commons/src/core/rac_error.cpp) and add case
RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION to return a string describing the backend
runtime ABI/version mismatch and case RAC_ERROR_JNI_EXCEPTION to return a string
describing a pending Java exception from a JNI callback; ensure the returned
messages match the comments in rac_error.h and follow the existing
formatting/style of other case branches.

Comment on lines +95 to +98
* When new callbacks are appended here, bump RAC_PLATFORM_ADAPTER_VERSION
* and bump RAC_PLATFORM_ADAPTER_VERSION_LATEST. Older consumers continue
* to work by setting their `version` to the version they were compiled
* against; the runtime only reads callbacks through that version.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Reference to an undefined version macro in docs.

Line [96] mentions RAC_PLATFORM_ADAPTER_VERSION_LATEST, but this header only defines RAC_PLATFORM_ADAPTER_VERSION. Please align the comment with actual symbols.

📝 Suggested doc correction
- *   When new callbacks are appended here, bump RAC_PLATFORM_ADAPTER_VERSION
- *   and bump RAC_PLATFORM_ADAPTER_VERSION_LATEST. Older consumers continue
+ *   When new callbacks are appended here, bump RAC_PLATFORM_ADAPTER_VERSION.
+ *   Older consumers continue
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* When new callbacks are appended here, bump RAC_PLATFORM_ADAPTER_VERSION
* and bump RAC_PLATFORM_ADAPTER_VERSION_LATEST. Older consumers continue
* to work by setting their `version` to the version they were compiled
* against; the runtime only reads callbacks through that version.
* When new callbacks are appended here, bump RAC_PLATFORM_ADAPTER_VERSION.
* Older consumers continue
* to work by setting their `version` to the version they were compiled
* against; the runtime only reads callbacks through that version.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/include/rac/core/rac_platform_adapter.h` around lines
95 - 98, The comment references a nonexistent macro
`RAC_PLATFORM_ADAPTER_VERSION_LATEST`; update the comment to match the actual
symbol names used in this header by either changing the text to only mention
`RAC_PLATFORM_ADAPTER_VERSION` (and explain bumping that macro when callbacks
are appended) or add/introduce a proper `RAC_PLATFORM_ADAPTER_VERSION_LATEST`
definition if intended; locate the comment near `RAC_PLATFORM_ADAPTER_VERSION`
and make the wording consistent with the defined macro so consumers can rely on
the documented symbol.

Comment on lines +39 to +41
namespace {
std::atomic<const rac_wakeword_provider_ops_t*> g_provider{nullptr};
} // namespace
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bind each service to the provider that created its backend handle.

The service creates backend_handle with whatever g_provider points to at init time, but every later operation reloads the global provider again. If rac_wakeword_provider_set() clears or replaces the provider while services are alive, destroy/process/unload/set_threshold can route an old handle through the wrong vtable or skip cleanup entirely. Store the provider pointer on WakewordService (or reject provider changes while services exist) and use that captured ops table for the lifetime of the handle.

Suggested fix
 struct WakewordService {
   ...
+  const rac_wakeword_provider_ops_t* provider = nullptr;
   rac_handle_t backend_handle = nullptr;
 };

-    const auto* provider = g_provider.load(std::memory_order_acquire);
-    if (provider && provider->create) {
+    service->provider = g_provider.load(std::memory_order_acquire);
+    if (service->provider && service->provider->create) {
         rac_handle_t backend_handle = nullptr;
-        rac_result_t rc =
-            provider->create(&service->config, &backend_handle, provider->user_data);
+        rac_result_t rc = service->provider->create(
+            &service->config, &backend_handle, service->provider->user_data);
         ...
     }

-    const auto* provider = g_provider.load(std::memory_order_acquire);
+    const auto* provider = service->provider;

Also applies to: 161-188, 211-219, 256-277, 309-321, 530-540, 607-633, 757-767, 852-865

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/features/wakeword/wakeword_service.cpp` around
lines 39 - 41, The service currently uses the global g_provider at init but
re-reads it for later ops, which can misroute old backend_handle through a new
vtable; modify WakewordService to capture and store the provider pointer (const
rac_wakeword_provider_ops_t*) used when creating backend_handle and use that
stored ops table for all lifecycle calls (destroy, process, unload,
set_threshold) instead of reading g_provider each time; alternatively, implement
a guard in rac_wakeword_provider_set() to reject provider changes while
WakewordService instances exist—update methods referencing g_provider to use the
instance field (and ensure constructor stores the provider) so cleanup always
uses the correct ops.

#ifndef RAC_JNI_SCOPE_H
#define RAC_JNI_SCOPE_H

#include <jni.h>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Where jni_scope.h is included =="
rg -n -C2 '#include ".*jni_scope\.h"|#include <jni\.h>' sdk/runanywhere-commons

echo
echo "== JNI-related build wiring =="
fd -i "CMakeLists.txt" sdk/runanywhere-commons | while read -r f; do
  echo "--- $f ---"
  rg -n -C2 "JNI|find_package\\(JNI|runanywhere_commons_jni\\.cpp|target_include_directories|ANDROID" "$f" || true
done

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 23247


🏁 Script executed:

# Check if backend JNI targets explicitly configure JNI_INCLUDE_DIRS
rg -A 5 "add_library.*_jni.*SHARED" sdk/runanywhere-commons/src/backends/*/CMakeLists.txt

# Also check for any conditional include wiring in backend CMakeLists
echo "=== Checking for JNI_INCLUDE_DIRS in backend CMakeLists ==="
fd -i "CMakeLists.txt" sdk/runanywhere-commons/src/backends | xargs rg "JNI_INCLUDE_DIRS" || echo "No JNI_INCLUDE_DIRS found in backend CMakeLists"

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 1948


Add explicit JNI_INCLUDE_DIRS to all backend JNI targets.

Backend JNI targets (rac_backend_onnx_jni, rac_backend_whispercpp_jni, rac_backend_llamacpp_jni, rac_backend_rag_jni) in sdk/runanywhere-commons/src/backends/*/CMakeLists.txt and sdk/runanywhere-commons/src/features/rag/CMakeLists.txt omit JNI_INCLUDE_DIRS from their target_include_directories() calls. The project-wide include_directories(SYSTEM ${JNI_INCLUDE_DIRS}) in the main CMakeLists.txt applies only on non-Android (for IDE support), leaving these targets unable to find <jni.h> during actual Android builds. Each backend JNI target must explicitly include ${JNI_INCLUDE_DIRS} in its target-level configuration.

🧰 Tools
🪛 Clang (14.0.6)

[error] 56-56: 'jni.h' file not found

(clang-diagnostic-error)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/jni/jni_scope.h` at line 56, The backend JNI
targets rac_backend_onnx_jni, rac_backend_whispercpp_jni,
rac_backend_llamacpp_jni, and rac_backend_rag_jni are missing
${JNI_INCLUDE_DIRS} in their target_include_directories calls; update each
CMakeLists.txt for these targets to add ${JNI_INCLUDE_DIRS} (as SYSTEM or
PUBLIC/PRIVATE as appropriate) to the target_include_directories() invocation so
that <jni.h> is available for Android builds (ensure you reference the exact
target names above when making the change).

Comment on lines +807 to +830
rac::jni::JniScope js(env, "llm_stream_callback_token");
jboolean continueGen = JNI_TRUE;

if (ctx->onTokenExpectsBytes) {
jsize len = static_cast<jsize>(strlen(token));
jbyteArray jToken = env->NewByteArray(len);
env->SetByteArrayRegion(
jToken,
0,
len,
reinterpret_cast<const jbyte*>(token)
);
continueGen = env->CallBooleanMethod(ctx->callback, ctx->onTokenMethod, jToken);
env->DeleteLocalRef(jToken);
auto jToken = js.new_byte_array(len);
if (!js.failed() && jToken) {
env->SetByteArrayRegion(jToken.get(), 0, len,
reinterpret_cast<const jbyte*>(token));
js.check("SetByteArrayRegion");
if (!js.failed()) {
continueGen = js.call_boolean_method(ctx->callback,
ctx->onTokenMethod, jToken.get());
}
}
} else {
jstring jToken = env->NewStringUTF(token);
continueGen = env->CallBooleanMethod(ctx->callback, ctx->onTokenMethod, jToken);
env->DeleteLocalRef(jToken);
auto jToken = js.new_string_utf(token);
if (!js.failed() && jToken) {
continueGen = js.call_boolean_method(ctx->callback,
ctx->onTokenMethod, jToken.get());
}
}

const bool hadException = env->ExceptionCheck();
if (hadException) {
env->ExceptionDescribe();
env->ExceptionClear();
}
const bool hadException = js.failed();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Stop the LLM stream when onToken() throws.

After JniScope clears a Java exception, this path returns RAC_TRUE, so native generation keeps running and racLlmComponentGenerateStreamWithCallback() can still complete successfully even though Kotlin stopped receiving tokens. Mark the context as failed and return RAC_FALSE so the waiter observes a real stream failure.

Suggested fix
-            if (hadException) {
-                // Ignore callback return value when JNI exception was thrown.
-                return RAC_TRUE;
-            }
+            if (hadException) {
+                {
+                    std::lock_guard<std::mutex> lock(ctx->mtx);
+                    ctx->has_error = true;
+                    ctx->error_code = RAC_ERROR_JNI_EXCEPTION;
+                    ctx->error_message = "onToken callback threw";
+                    ctx->is_complete = true;
+                }
+                ctx->cv.notify_one();
+                return RAC_FALSE;
+            }

Also applies to: 836-839

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp` around lines 807
- 830, The code currently ignores Java exceptions cleared by rac::jni::JniScope
(js.failed() / hadException) and continues generation returning RAC_TRUE; update
the token-callback handling in racLlmComponentGenerateStreamWithCallback's loop
so that if js.failed() (i.e., hadException is true) after calling
ctx->onTokenMethod on ctx->callback (both byte and string paths) you mark the
context as failed and return RAC_FALSE immediately; ensure you reference
JniScope js, ctx->onTokenExpectsBytes, ctx->callback and ctx->onTokenMethod and
replace the current unconditional RAC_TRUE return with a failure path that stops
the stream.

Comment on lines +2859 to +2864
rac::jni::JniScope s(env, "jni_device_get_info");
auto cb_result = s.call_object_method(g_device_jni_state.callback_obj,
g_device_jni_state.get_device_info_method);
RAC_JNI_TRY_VOID(s);

// Check for Java exception after CallObjectMethod
if (env->ExceptionCheck()) {
LOGe("jni_device_get_info: Java exception occurred in getDeviceInfo()");
env->ExceptionDescribe();
env->ExceptionClear();
return;
}
jstring jResult = static_cast<jstring>(cb_result.get());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Check GetStringUTFChars() before logging and parsing jResult.

GetStringUTFChars() can return nullptr, but this callback immediately logs json_str, passes it to json::parse, and releases it unconditionally. That turns an OOM/JNI failure into undefined behavior instead of a clean early return.

Suggested fix
-        const char* json_str = env->GetStringUTFChars(jResult, nullptr);
-        LOGd("jni_device_get_info: parsing JSON: %.200s...", json_str);
+        const char* json_str = env->GetStringUTFChars(jResult, nullptr);
+        s.check("GetStringUTFChars");
+        if (json_str == nullptr || s.failed()) {
+            return;
+        }
+        LOGd("jni_device_get_info: parsing JSON: %.200s...", json_str);
...
-        env->ReleaseStringUTFChars(jResult, json_str);
+        env->ReleaseStringUTFChars(jResult, json_str);

Also applies to: 2866-2949

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp` around lines
2859 - 2864, In jni_device_get_info handling, guard against GetStringUTFChars
returning null: after obtaining jstring jResult from cb_result.get() and before
calling env->GetStringUTFChars (or using json_str), check that jResult is
non-null and that the returned const char* from GetStringUTFChars is not null;
if either is null, call RAC_JNI_TRY_VOID(s) or return early (releasing resources
appropriately) instead of proceeding to log or json::parse; update the code
paths around rac::jni::JniScope s(...), cb_result,
g_device_jni_state.get_device_info_method, and any subsequent json::parse and
env->ReleaseStringUTFChars calls to only run when the UTF chars pointer is valid
so we avoid UB on OOM/JNI failures.

Comment on lines +368 to +377
/** Backend unavailable: backend compiled as stub, engine binary not installed */
#define RAC_ERROR_BACKEND_UNAVAILABLE ((rac_result_t) - 604)
/** Backend runtime ABI is incompatible with the version the SDK was compiled
* against. Typically means the prebuilt third_party binary (sherpa-onnx,
* ONNX Runtime, etc.) was produced from a different major/minor version
* than the headers; struct layouts may not match, so we refuse to proceed. */
#define RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION ((rac_result_t) - 605)
/** JNI call from a Kotlin/Java callback left a pending Java exception on
* the JNIEnv - caught by JniScope, logged, and cleared. */
#define RAC_ERROR_JNI_EXCEPTION ((rac_result_t) - 606)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate rac_error.cpp and verify new code coverage"
fd -i '^rac_error\.cpp$' | while read -r f; do
  echo "== $f =="

  echo "-- New constants referenced --"
  rg -n -C2 'RAC_ERROR_BACKEND_UNAVAILABLE|RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION|RAC_ERROR_JNI_EXCEPTION' "$f"

  echo "-- switch-case mappings in rac_error_message() --"
  rg -n -C2 'case\s+RAC_ERROR_BACKEND_UNAVAILABLE|case\s+RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION|case\s+RAC_ERROR_JNI_EXCEPTION' "$f"
done

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 747


🏁 Script executed:

#!/bin/bash
# Extract the rac_error_message() function to see all switch cases
fd -i '^rac_error\.cpp$' | head -1 | xargs cat | \
  awk '/^[a-zA-Z_].*rac_error_message\(/{flag=1} flag{print NR": "$0} /^}$/{if(flag && prev_brace++) exit}' | head -100

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 4882


🏁 Script executed:

#!/bin/bash
# Extract rac_error_message() function and show lines around 310
fd -i '^rac_error\.cpp$' | head -1 | xargs sed -n '250,350p'

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 4364


Add message mappings for RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION and RAC_ERROR_JNI_EXCEPTION in rac_error_message().

Two of the three new error codes (-605 and -606) are missing case statements in rac_error.cpp. Only RAC_ERROR_BACKEND_UNAVAILABLE (-604) has a mapping. Without entries in the switch statement, callers will receive "Unknown error code" instead of meaningful error messages.

Add the following cases to the BACKEND ERRORS section in sdk/runanywhere-commons/src/core/rac_error.cpp:

case RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION:
    return "Backend ABI incompatible with SDK version";
case RAC_ERROR_JNI_EXCEPTION:
    return "JNI exception occurred";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_error.h`
around lines 368 - 377, The switch in rac_error_message() is missing case
mappings for RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION and RAC_ERROR_JNI_EXCEPTION,
so callers get "Unknown error code"; add two cases in the BACKEND ERRORS section
of rac_error.cpp within rac_error_message(): one for
RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION that returns "Backend ABI incompatible
with SDK version" and one for RAC_ERROR_JNI_EXCEPTION that returns "JNI
exception occurred".

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
sdk/runanywhere-commons/src/core/rac_core.cpp (1)

127-131: ⚠️ Potential issue | 🟡 Minor

Remove the {0}-init placeholder from the error detail.

Line 130 still contains an unexpanded template token, so callers will see {0}-init verbatim in diagnostics.

🛠️ Suggested wording
         rac_error_set_details(
             "Platform adapter .version is 0. Every caller must set "
             "`.version = RAC_PLATFORM_ADAPTER_VERSION` before passing the "
-            "adapter to rac_init(). If you are using {0}-init, add the "
-            "assignment explicitly.");
+            "adapter to rac_init(). Set the field explicitly during adapter "
+            "initialization.");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/core/rac_core.cpp` around lines 127 - 131, Update
the error text passed to rac_error_set_details to remove the unexpanded
`{0}-init` placeholder: change the message in the rac_error_set_details call
that mentions ".version is 0" so it no longer contains `{0}-init` and instead
uses a plain instruction (e.g., "If you are using a platform-specific init, set
`.version = RAC_PLATFORM_ADAPTER_VERSION` before calling rac_init()."); keep
references to `.version`, RAC_PLATFORM_ADAPTER_VERSION and rac_init() so callers
know what to change.
🧹 Nitpick comments (3)
sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_hardware.h (1)

98-98: Add RAC_NODISCARD to the formatted-report allocator.

Line 98 returns newly allocated memory; if the return value is accidentally ignored, that leaks memory silently. Marking it RAC_NODISCARD helps catch misuse at compile time.

Proposed diff
-RAC_API char* rac_hardware_format_report(const rac_hardware_report_t* report);
+RAC_API RAC_NODISCARD char* rac_hardware_format_report(const rac_hardware_report_t* report);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_hardware.h`
at line 98, The declaration for rac_hardware_format_report should be annotated
with RAC_NODISCARD to prevent callers from silently ignoring the newly allocated
return; update the prototype for rac_hardware_format_report in rac_hardware.h
(the declaration "rac_hardware_format_report(const rac_hardware_report_t*
report)") to include the RAC_NODISCARD attribute before the return type so
compilers can warn when the returned char* is discarded.
sdk/runanywhere-commons/include/rac/features/wakeword/rac_wakeword_service.h (1)

419-431: Prefer enforced runtime checks over “undefined behaviour” for re-registration.

Given this is exported API, reject unsafe provider swaps/clears while services are alive (e.g., RAC_ERROR_INVALID_STATE) instead of relying only on contract text.

Also applies to: 437-437

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/include/rac/features/wakeword/rac_wakeword_service.h`
around lines 419 - 431, The public provider registration function (the one that
sets/clears the backend ops used by rac_wakeword_create) must enforce the
contract at runtime: add a check in rac_wakeword_register_provider (or the
provider-setter function) to refuse swapping or clearing the ops while any
wake-word service instances exist by returning an error code such as
RAC_ERROR_INVALID_STATE; only allow setting NULL when the active-service count
is zero. Track active services (increment in rac_wakeword_create, decrement in
rac_wakeword_destroy) and protect both the count and the provider pointer with
the same mutex to avoid races, returning the error immediately instead of
leaving the behaviour undefined.
sdk/runanywhere-commons/src/core/rac_core.cpp (1)

134-138: Make the runtime check match the documented ABI contract.

rac_platform_adapter_t.version is documented as “must equal RAC_PLATFORM_ADAPTER_VERSION”, but this branch only rejects 0 and newer versions. After the next version bump, stale adapters will still pass init unless equality is enforced or backward compatibility is documented explicitly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/core/rac_core.cpp` around lines 134 - 138, The
platform adapter version check currently only rejects adapters newer than
RAC_PLATFORM_ADAPTER_VERSION; change the comparison in the initialization path
that reads rac_platform_adapter_t.version to enforce equality with
RAC_PLATFORM_ADAPTER_VERSION (i.e., fail when version !=
RAC_PLATFORM_ADAPTER_VERSION) so stale or future adapter versions are rejected
per the documented ABI contract; update the rac_error_set_details message to
state that the adapter version must equal RAC_PLATFORM_ADAPTER_VERSION and
return RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION from the same branch (refer to
rac_platform_adapter_t.version and RAC_PLATFORM_ADAPTER_VERSION in
rac_core.cpp).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@sdk/runanywhere-commons/include/rac/features/wakeword/rac_wakeword_service.h`:
- Line 445: The function rac_wakeword_has_provider currently returns rac_bool_t
and must be converted to the rac_result_t convention: change the public
declaration to rac_result_t rac_wakeword_has_provider(rac_bool_t*
out_has_provider) and update the implementation to validate out_has_provider
(return appropriate rac_result_t on null), set *out_has_provider to the boolean
result, and return RAC_RESULT_OK or an error code on failure; update any
internal helper functions, callers, and unit tests to use the new pointer output
pattern and handle rac_result_t checks accordingly, keeping the RAC_API export
macro and function name unchanged except for the new signature.
- Around line 362-411: The public vtable rac_wakeword_provider_ops_t can break
ABI when fields change; add explicit ABI fields (e.g. uint32_t abi_version and
size_t struct_size) as the first members of rac_wakeword_provider_ops_t and
document the current ABI_VERSION constant; then update
rac_wakeword_provider_set() to validate the incoming ops by checking
ops->abi_version equals the expected ABI_VERSION and ops->struct_size is at
least the minimum expected size (or equals sizeof the known struct) and return a
rac_result_t error if validation fails, ensuring all subsequent code uses only
fields within the validated struct_size.

In `@sdk/runanywhere-commons/src/core/rac_core.cpp`:
- Around line 120-139: The ABI-version check added to rac_init() must be
duplicated in rac_set_platform_adapter(): in rac_set_platform_adapter(),
validate platform_adapter->version (reject 0 and versions >
RAC_PLATFORM_ADAPTER_VERSION) and call rac_error_set_details with a matching
explanatory message and return RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION on
failure; ensure you reference the same error texts and constants
(RAC_PLATFORM_ADAPTER_VERSION, rac_error_set_details,
RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION, and platform_adapter->version) so the
adapter cannot be installed with an unversioned or too-new layout.

---

Duplicate comments:
In `@sdk/runanywhere-commons/src/core/rac_core.cpp`:
- Around line 127-131: Update the error text passed to rac_error_set_details to
remove the unexpanded `{0}-init` placeholder: change the message in the
rac_error_set_details call that mentions ".version is 0" so it no longer
contains `{0}-init` and instead uses a plain instruction (e.g., "If you are
using a platform-specific init, set `.version = RAC_PLATFORM_ADAPTER_VERSION`
before calling rac_init()."); keep references to `.version`,
RAC_PLATFORM_ADAPTER_VERSION and rac_init() so callers know what to change.

---

Nitpick comments:
In
`@sdk/runanywhere-commons/include/rac/features/wakeword/rac_wakeword_service.h`:
- Around line 419-431: The public provider registration function (the one that
sets/clears the backend ops used by rac_wakeword_create) must enforce the
contract at runtime: add a check in rac_wakeword_register_provider (or the
provider-setter function) to refuse swapping or clearing the ops while any
wake-word service instances exist by returning an error code such as
RAC_ERROR_INVALID_STATE; only allow setting NULL when the active-service count
is zero. Track active services (increment in rac_wakeword_create, decrement in
rac_wakeword_destroy) and protect both the count and the provider pointer with
the same mutex to avoid races, returning the error immediately instead of
leaving the behaviour undefined.

In `@sdk/runanywhere-commons/src/core/rac_core.cpp`:
- Around line 134-138: The platform adapter version check currently only rejects
adapters newer than RAC_PLATFORM_ADAPTER_VERSION; change the comparison in the
initialization path that reads rac_platform_adapter_t.version to enforce
equality with RAC_PLATFORM_ADAPTER_VERSION (i.e., fail when version !=
RAC_PLATFORM_ADAPTER_VERSION) so stale or future adapter versions are rejected
per the documented ABI contract; update the rac_error_set_details message to
state that the adapter version must equal RAC_PLATFORM_ADAPTER_VERSION and
return RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION from the same branch (refer to
rac_platform_adapter_t.version and RAC_PLATFORM_ADAPTER_VERSION in
rac_core.cpp).

In `@sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_hardware.h`:
- Line 98: The declaration for rac_hardware_format_report should be annotated
with RAC_NODISCARD to prevent callers from silently ignoring the newly allocated
return; update the prototype for rac_hardware_format_report in rac_hardware.h
(the declaration "rac_hardware_format_report(const rac_hardware_report_t*
report)") to include the RAC_NODISCARD attribute before the return type so
compilers can warn when the returned char* is discarded.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 57eb33c0-4705-44ee-9f86-ee1d12613a6f

📥 Commits

Reviewing files that changed from the base of the PR and between 459c324 and 2ed7fb2.

📒 Files selected for processing (14)
  • sdk/runanywhere-commons/include/rac/backends/rac_backend_metalrt.h
  • sdk/runanywhere-commons/include/rac/backends/rac_llm_llamacpp.h
  • sdk/runanywhere-commons/include/rac/backends/rac_stt_onnx.h
  • sdk/runanywhere-commons/include/rac/backends/rac_stt_whispercpp.h
  • sdk/runanywhere-commons/include/rac/backends/rac_tts_onnx.h
  • sdk/runanywhere-commons/include/rac/backends/rac_vad_onnx.h
  • sdk/runanywhere-commons/include/rac/backends/rac_vlm_llamacpp.h
  • sdk/runanywhere-commons/include/rac/backends/rac_wakeword_onnx.h
  • sdk/runanywhere-commons/include/rac/features/wakeword/rac_wakeword_service.h
  • sdk/runanywhere-commons/src/core/rac_core.cpp
  • sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/CRACommons.h
  • sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_attrs.h
  • sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_hardware.h
  • sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_types.h
✅ Files skipped from review due to trivial changes (4)
  • sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_types.h
  • sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/CRACommons.h
  • sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_attrs.h
  • sdk/runanywhere-commons/include/rac/backends/rac_wakeword_onnx.h

Comment on lines +362 to +411
typedef struct rac_wakeword_provider_ops {
/** Create a backend-specific handle. Called once per wake-word service
* during rac_wakeword_initialize(). Must return a handle suitable for
* passing to every other vtable method (and to `destroy`). */
rac_result_t (*create)(const rac_wakeword_config_t* config,
rac_handle_t* out_backend_handle,
void* user_data);

/** Load a wake-word classification model (ONNX, etc.). */
rac_result_t (*load_model)(rac_handle_t backend_handle,
const char* model_path,
const char* model_id,
const char* wake_word,
void* user_data);

/** Unload a wake-word model previously loaded with load_model. */
rac_result_t (*unload_model)(rac_handle_t backend_handle,
const char* model_id,
void* user_data);

/** Load a VAD pre-filter model (Silero). Optional. */
rac_result_t (*load_vad)(rac_handle_t backend_handle,
const char* vad_model_path,
void* user_data);

/** Run one frame of audio through inference + optional VAD.
* out_detected_index is set to >= 0 if a wake word fires (index into
* the provider's loaded-models list) or -1 otherwise. */
rac_result_t (*process)(rac_handle_t backend_handle,
const float* samples, size_t num_samples,
int32_t* out_detected_index,
float* out_confidence,
rac_bool_t* out_vad_speech,
float* out_vad_confidence,
void* user_data);

/** Reset internal state (KV cache, sliding window, etc.). */
rac_result_t (*reset)(rac_handle_t backend_handle, void* user_data);

/** Adjust detection threshold at runtime. */
rac_result_t (*set_threshold)(rac_handle_t backend_handle,
float threshold,
void* user_data);

/** Tear down the backend handle. */
void (*destroy)(rac_handle_t backend_handle, void* user_data);

/** Opaque user-data passed through to every callback. */
void* user_data;
} rac_wakeword_provider_ops_t;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add ABI version/size fields to rac_wakeword_provider_ops_t before this interface hardens.

This public vtable can silently break binary compatibility when fields are added/reordered. Add an explicit version/size contract and validate it in rac_wakeword_provider_set().

Proposed header-side ABI hardening
+// Increment when `rac_wakeword_provider_ops_t` layout changes incompatibly.
+#define RAC_WAKEWORD_PROVIDER_OPS_VERSION 1u
+
 typedef struct rac_wakeword_provider_ops {
+    /** ABI version for this vtable layout. */
+    uint32_t version;
+    /** sizeof(rac_wakeword_provider_ops_t) from provider compile unit. */
+    uint32_t struct_size;
+
     /** Create a backend-specific handle. Called once per wake-word service
      *  during rac_wakeword_initialize(). Must return a handle suitable for
      *  passing to every other vtable method (and to `destroy`). */
     rac_result_t (*create)(const rac_wakeword_config_t* config,
                            rac_handle_t* out_backend_handle,
                            void* user_data);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
typedef struct rac_wakeword_provider_ops {
/** Create a backend-specific handle. Called once per wake-word service
* during rac_wakeword_initialize(). Must return a handle suitable for
* passing to every other vtable method (and to `destroy`). */
rac_result_t (*create)(const rac_wakeword_config_t* config,
rac_handle_t* out_backend_handle,
void* user_data);
/** Load a wake-word classification model (ONNX, etc.). */
rac_result_t (*load_model)(rac_handle_t backend_handle,
const char* model_path,
const char* model_id,
const char* wake_word,
void* user_data);
/** Unload a wake-word model previously loaded with load_model. */
rac_result_t (*unload_model)(rac_handle_t backend_handle,
const char* model_id,
void* user_data);
/** Load a VAD pre-filter model (Silero). Optional. */
rac_result_t (*load_vad)(rac_handle_t backend_handle,
const char* vad_model_path,
void* user_data);
/** Run one frame of audio through inference + optional VAD.
* out_detected_index is set to >= 0 if a wake word fires (index into
* the provider's loaded-models list) or -1 otherwise. */
rac_result_t (*process)(rac_handle_t backend_handle,
const float* samples, size_t num_samples,
int32_t* out_detected_index,
float* out_confidence,
rac_bool_t* out_vad_speech,
float* out_vad_confidence,
void* user_data);
/** Reset internal state (KV cache, sliding window, etc.). */
rac_result_t (*reset)(rac_handle_t backend_handle, void* user_data);
/** Adjust detection threshold at runtime. */
rac_result_t (*set_threshold)(rac_handle_t backend_handle,
float threshold,
void* user_data);
/** Tear down the backend handle. */
void (*destroy)(rac_handle_t backend_handle, void* user_data);
/** Opaque user-data passed through to every callback. */
void* user_data;
} rac_wakeword_provider_ops_t;
// Increment when `rac_wakeword_provider_ops_t` layout changes incompatibly.
`#define` RAC_WAKEWORD_PROVIDER_OPS_VERSION 1u
typedef struct rac_wakeword_provider_ops {
/** ABI version for this vtable layout. */
uint32_t version;
/** sizeof(rac_wakeword_provider_ops_t) from provider compile unit. */
uint32_t struct_size;
/** Create a backend-specific handle. Called once per wake-word service
* during rac_wakeword_initialize(). Must return a handle suitable for
* passing to every other vtable method (and to `destroy`). */
rac_result_t (*create)(const rac_wakeword_config_t* config,
rac_handle_t* out_backend_handle,
void* user_data);
/** Load a wake-word classification model (ONNX, etc.). */
rac_result_t (*load_model)(rac_handle_t backend_handle,
const char* model_path,
const char* model_id,
const char* wake_word,
void* user_data);
/** Unload a wake-word model previously loaded with load_model. */
rac_result_t (*unload_model)(rac_handle_t backend_handle,
const char* model_id,
void* user_data);
/** Load a VAD pre-filter model (Silero). Optional. */
rac_result_t (*load_vad)(rac_handle_t backend_handle,
const char* vad_model_path,
void* user_data);
/** Run one frame of audio through inference + optional VAD.
* out_detected_index is set to >= 0 if a wake word fires (index into
* the provider's loaded-models list) or -1 otherwise. */
rac_result_t (*process)(rac_handle_t backend_handle,
const float* samples, size_t num_samples,
int32_t* out_detected_index,
float* out_confidence,
rac_bool_t* out_vad_speech,
float* out_vad_confidence,
void* user_data);
/** Reset internal state (KV cache, sliding window, etc.). */
rac_result_t (*reset)(rac_handle_t backend_handle, void* user_data);
/** Adjust detection threshold at runtime. */
rac_result_t (*set_threshold)(rac_handle_t backend_handle,
float threshold,
void* user_data);
/** Tear down the backend handle. */
void (*destroy)(rac_handle_t backend_handle, void* user_data);
/** Opaque user-data passed through to every callback. */
void* user_data;
} rac_wakeword_provider_ops_t;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/include/rac/features/wakeword/rac_wakeword_service.h`
around lines 362 - 411, The public vtable rac_wakeword_provider_ops_t can break
ABI when fields change; add explicit ABI fields (e.g. uint32_t abi_version and
size_t struct_size) as the first members of rac_wakeword_provider_ops_t and
document the current ABI_VERSION constant; then update
rac_wakeword_provider_set() to validate the incoming ops by checking
ops->abi_version equals the expected ABI_VERSION and ops->struct_size is at
least the minimum expected size (or equals sizeof the known struct) and return a
rac_result_t error if validation fails, ensuring all subsequent code uses only
fields within the validated struct_size.

* @return RAC_TRUE if a provider has been set via rac_wakeword_provider_set(),
* RAC_FALSE otherwise. Useful for test code and graceful degradation.
*/
RAC_API rac_bool_t rac_wakeword_has_provider(void);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

rac_wakeword_has_provider should follow rac_result_t error-handling convention.

Returning rac_bool_t directly introduces a new inconsistency in the public C API surface. Consider rac_result_t rac_wakeword_has_provider(rac_bool_t* out_has_provider).

As per coding guidelines, “All public API functions must be declared in include/rac/ header files and implement consistent error handling via rac_result_t.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/include/rac/features/wakeword/rac_wakeword_service.h`
at line 445, The function rac_wakeword_has_provider currently returns rac_bool_t
and must be converted to the rac_result_t convention: change the public
declaration to rac_result_t rac_wakeword_has_provider(rac_bool_t*
out_has_provider) and update the implementation to validate out_has_provider
(return appropriate rac_result_t on null), set *out_has_provider to the boolean
result, and return RAC_RESULT_OK or an error code on failure; update any
internal helper functions, callers, and unit tests to use the new pointer output
pattern and handle rac_result_t checks accordingly, keeping the RAC_API export
macro and function name unchanged except for the new signature.

Comment on lines +120 to +139
// Adapter ABI version check. Every caller MUST set
// `.version = RAC_PLATFORM_ADAPTER_VERSION` before calling rac_init.
// No grace period - an unversioned adapter is a programming error and
// we'd rather fail loudly at init than hit UB later when the runtime
// tries to call a callback at a wrong offset.
const uint32_t adapter_version = config->platform_adapter->version;
if (adapter_version == 0) {
rac_error_set_details(
"Platform adapter .version is 0. Every caller must set "
"`.version = RAC_PLATFORM_ADAPTER_VERSION` before passing the "
"adapter to rac_init(). If you are using {0}-init, add the "
"assignment explicitly.");
return RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION;
}
if (adapter_version > RAC_PLATFORM_ADAPTER_VERSION) {
rac_error_set_details(
"Platform adapter version is newer than this runtime supports. "
"Upgrade runanywhere-commons or set .version to a supported value.");
return RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate adapters in rac_set_platform_adapter() too.

This ABI gate only protects rac_init(). rac_set_platform_adapter() at Lines 68-74 can still install an unversioned or future-layout adapter, which reintroduces the callback-offset risk this change is meant to prevent.

🛠️ Suggested extraction
+static rac_result_t validate_platform_adapter_version(
+    const rac_platform_adapter_t* adapter) {
+    if (adapter == nullptr) {
+        return RAC_ERROR_NULL_POINTER;
+    }
+
+    const uint32_t adapter_version = adapter->version;
+    if (adapter_version == 0) {
+        rac_error_set_details(
+            "Platform adapter .version is 0. Set "
+            "`.version = RAC_PLATFORM_ADAPTER_VERSION` before passing the "
+            "adapter to rac_init().");
+        return RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION;
+    }
+    if (adapter_version != RAC_PLATFORM_ADAPTER_VERSION) {
+        rac_error_set_details(
+            "Platform adapter version is incompatible with this runtime.");
+        return RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION;
+    }
+    return RAC_SUCCESS;
+}
+
 rac_result_t rac_set_platform_adapter(const rac_platform_adapter_t* adapter) {
-    if (adapter == nullptr) {
-        return RAC_ERROR_NULL_POINTER;
+    rac_result_t result = validate_platform_adapter_version(adapter);
+    if (result != RAC_SUCCESS) {
+        return result;
     }
     s_platform_adapter = adapter;
     return RAC_SUCCESS;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/core/rac_core.cpp` around lines 120 - 139, The
ABI-version check added to rac_init() must be duplicated in
rac_set_platform_adapter(): in rac_set_platform_adapter(), validate
platform_adapter->version (reject 0 and versions > RAC_PLATFORM_ADAPTER_VERSION)
and call rac_error_set_details with a matching explanatory message and return
RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION on failure; ensure you reference the same
error texts and constants (RAC_PLATFORM_ADAPTER_VERSION, rac_error_set_details,
RAC_ERROR_BACKEND_INCOMPATIBLE_VERSION, and platform_adapter->version) so the
adapter cannot be installed with an unversioned or too-new layout.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant