Skip to content

Commit 698fe6f

Browse files
Address C12+C14: MetalRT stub pattern for public-repo compile
The MetalRT engine (libmetalrt_engine.a + .metallib shaders) is a closed-source private dependency. Public repo users could not compile with RAC_BACKEND_METALRT=ON without access to the private engine. This change introduces a stub pattern that lets the public repo compile cleanly, and fails with a clear runtime error when MetalRT inference is requested without the engine binary installed. Pattern (extensible to future private backends, e.g. Qualcomm NPU): 1. Compile-time (CMake): New RAC_METALRT_ENGINE_AVAILABLE option. OFF by default (public): the stub header + stub .c file under src/backends/metalrt/stubs/ are compiled in, every metalrt_* symbol resolves to a no-op. ON (internal): the real private metalrt_c_api.h from METALRT_ROOT is used and libmetalrt_engine.a is linked with find_library REQUIRED (configure fails hard if engine claimed-available but missing, matching llamacpp/onnx behavior). 2. Fixes the C12 orphan-target concern: rac_backend_metalrt is now an OBJECT library folded into rac_commons via target_sources(rac_commons PRIVATE $<TARGET_OBJECTS:rac_backend_metalrt>) mirroring the RAG pattern. No separate librac_backend_metalrt.a to distribute — wrappers ride in librac_commons.a (and RACommons xcframework on iOS); engine + tokenizers ship separately in RABackendMetalRT.xcframework when the authorized developer enables it. 3. Runtime: rac_backend_metalrt_register() on a stub build logs a clear warning and skips provider registration entirely. The service registry then returns BACKEND_NOT_FOUND at loadModel call sites, which Swift maps to SDKError.componentNotAvailable. No crashes, no silent no-ops. metalrt_can_handle also short-circuits to RAC_FALSE as belt-and-suspenders. 4. New error code RAC_ERROR_BACKEND_UNAVAILABLE = -604 in the backend range (-600..-699) for callers that need to distinguish "backend not found" from "backend compiled as stub". Error message added. 5. build-ios.sh: passes -DRAC_METALRT_ENGINE_AVAILABLE=ON whenever --backend metalrt is selected, so the internal MetalRT iOS build continues to work end-to-end (RACommons xcframework with real wrappers + RABackendMetalRT xcframework with engine + tokenizers). Files: - src/backends/metalrt/stubs/metalrt_c_api.h (new, public stub) - src/backends/metalrt/stubs/metalrt_c_api_stub.c (new, no-op) - src/backends/metalrt/CMakeLists.txt (two-branch + OBJECT) - CMakeLists.txt (fold rac_backend_metalrt into rac_commons) - src/backends/metalrt/rac_backend_metalrt_register.cpp (short-circuit can_handle + register on stub builds) - include/rac/core/rac_error.h + src/core/rac_error.cpp (RAC_ERROR_BACKEND_UNAVAILABLE) - scripts/build-ios.sh (pass RAC_METALRT_ENGINE_AVAILABLE=ON) Verified: all 5 MetalRT wrapper .cpp files compile cleanly against the stub header with clang -std=c++20 -DRAC_METALRT_ENGINE_AVAILABLE=0. Default build (RAC_BUILD_BACKENDS=OFF) still configures cleanly. When the MetalRT project is open-sourced or moved into a private SPM binary pipeline, the only change needed is flipping RAC_METALRT_ENGINE_AVAILABLE=ON plus wiring the binary fetch — the stub and wrappers stay untouched.
1 parent 5ff6e30 commit 698fe6f

9 files changed

Lines changed: 445 additions & 60 deletions

File tree

examples/ios/RunAnywhereAI/Package.resolved

Lines changed: 0 additions & 27 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sdk/runanywhere-commons/CMakeLists.txt

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,8 +507,32 @@ if(RAC_BUILD_BACKENDS)
507507
endif()
508508

509509
if(RAC_BACKEND_METALRT AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/backends/metalrt/CMakeLists.txt")
510-
message(STATUS " - MetalRT backend")
510+
message(STATUS " - MetalRT backend — folded into rac_commons")
511511
add_subdirectory(src/backends/metalrt)
512+
513+
# MetalRT is an OBJECT library. Fold its compiled objects into
514+
# rac_commons so there is no separate rac_backend_metalrt artifact
515+
# to distribute — mirrors how RAG is integrated. Public builds ship
516+
# stub implementations; engine-enabled builds link the private
517+
# libmetalrt_engine.a via RAC_METALRT_ENGINE_LIB propagated from
518+
# the subdirectory.
519+
target_sources(rac_commons PRIVATE $<TARGET_OBJECTS:rac_backend_metalrt>)
520+
521+
# Apple frameworks are needed wherever the wrappers are compiled.
522+
if(APPLE)
523+
target_link_libraries(rac_commons PUBLIC
524+
"-framework Metal"
525+
"-framework Foundation"
526+
"-framework Accelerate"
527+
)
528+
endif()
529+
530+
# If the engine is available, link the private static lib onto
531+
# rac_commons so the OBJECT target's unresolved metalrt_* symbols
532+
# resolve in the final archive.
533+
if(RAC_METALRT_ENGINE_LIB)
534+
target_link_libraries(rac_commons PUBLIC ${RAC_METALRT_ENGINE_LIB})
535+
endif()
512536
endif()
513537

514538
if(RAC_BACKEND_RAG AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/features/rag/CMakeLists.txt")

sdk/runanywhere-commons/include/rac/core/rac_error.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,8 @@ extern "C" {
365365
#define RAC_ERROR_BACKEND_INIT_FAILED ((rac_result_t) - 602)
366366
/** Backend busy */
367367
#define RAC_ERROR_BACKEND_BUSY ((rac_result_t) - 603)
368+
/** Backend unavailable: backend compiled as stub, engine binary not installed */
369+
#define RAC_ERROR_BACKEND_UNAVAILABLE ((rac_result_t) - 604)
368370
/** Invalid handle */
369371
#define RAC_ERROR_INVALID_HANDLE ((rac_result_t) - 610)
370372

sdk/runanywhere-commons/scripts/build-ios.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ build_macos() {
204204
;;
205205
metalrt)
206206
BACKEND_FLAGS="$BACKEND_FLAGS -DRAC_BACKEND_LLAMACPP=OFF -DRAC_BACKEND_ONNX=OFF -DRAC_BACKEND_WHISPERCPP=OFF -DRAC_BACKEND_METALRT=ON"
207+
BACKEND_FLAGS="$BACKEND_FLAGS -DRAC_METALRT_ENGINE_AVAILABLE=ON"
207208
BACKEND_FLAGS="$BACKEND_FLAGS -DMETALRT_ROOT=${METALRT_ROOT}"
208209
;;
209210
all|*)
@@ -311,6 +312,7 @@ build_platform() {
311312
;;
312313
metalrt)
313314
BACKEND_FLAGS="$BACKEND_FLAGS -DRAC_BACKEND_LLAMACPP=OFF -DRAC_BACKEND_ONNX=OFF -DRAC_BACKEND_WHISPERCPP=OFF -DRAC_BACKEND_METALRT=ON"
315+
BACKEND_FLAGS="$BACKEND_FLAGS -DRAC_METALRT_ENGINE_AVAILABLE=ON"
314316
BACKEND_FLAGS="$BACKEND_FLAGS -DMETALRT_ROOT=${METALRT_ROOT}"
315317
if [[ -n "${METALRT_IOS_BUILD_DIR}" ]]; then
316318
BACKEND_FLAGS="$BACKEND_FLAGS -DMETALRT_LIB_DIR=${METALRT_IOS_BUILD_DIR}"

sdk/runanywhere-commons/src/backends/metalrt/CMakeLists.txt

Lines changed: 70 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,94 @@
11
# MetalRT Backend — Apple-only (iOS/macOS)
2-
# Requires pre-built libmetalrt_engine.a and metalrt_c_api.h
2+
#
3+
# The MetalRT engine (libmetalrt_engine.a + metalrt_c_api.h) is a private,
4+
# closed-source dependency. This CMakeLists supports two modes:
5+
#
6+
# 1) Engine NOT available (default in the public repo):
7+
# - RAC_METALRT_ENGINE_AVAILABLE is OFF.
8+
# - The public stub header and stub source under stubs/ are compiled in.
9+
# - Every metalrt_* symbol resolves to a no-op returning a sentinel.
10+
# - The public repo links cleanly. At runtime, the wrappers + vtable
11+
# registration short-circuit to RAC_ERROR_BACKEND_UNAVAILABLE before
12+
# invoking these stubs, so loadModel(..., framework: .metalrt) returns
13+
# a clean error instead of a crash.
14+
#
15+
# 2) Engine IS available (internal / authorized builds):
16+
# - RAC_METALRT_ENGINE_AVAILABLE=ON.
17+
# - METALRT_ROOT must point at the private MetalRT project.
18+
# - The real metalrt_c_api.h from METALRT_ROOT/src is used; stubs are
19+
# skipped. libmetalrt_engine.a is linked via find_library (REQUIRED).
20+
# - Configure fails hard if the engine is claimed-available but missing,
21+
# consistent with how llamacpp/onnx handle missing deps.
322

4-
set(METALRT_SOURCES
23+
option(RAC_METALRT_ENGINE_AVAILABLE
24+
"Link the private MetalRT engine (libmetalrt_engine.a). OFF = compile stubs only."
25+
OFF)
26+
27+
set(METALRT_WRAPPER_SOURCES
528
rac_llm_metalrt.cpp
629
rac_stt_metalrt.cpp
730
rac_tts_metalrt.cpp
831
rac_vlm_metalrt.cpp
932
rac_backend_metalrt_register.cpp
1033
)
1134

12-
# MetalRT pre-built library location (set by parent or via -DMETALRT_ROOT=...)
13-
if(NOT DEFINED METALRT_ROOT)
14-
# Default: assume MetalRT is sibling to runanywhere-sdks
15-
set(METALRT_ROOT "${CMAKE_SOURCE_DIR}/../../../MetalRT" CACHE PATH "Path to MetalRT project root")
16-
endif()
17-
18-
set(METALRT_INCLUDE_DIR "${METALRT_ROOT}/src" CACHE PATH "Path to metalrt_c_api.h")
19-
20-
# Find the pre-built static library
21-
if(DEFINED METALRT_LIB_DIR)
22-
set(_metalrt_lib_dir "${METALRT_LIB_DIR}")
23-
else()
24-
set(_metalrt_lib_dir "${METALRT_ROOT}/build")
25-
endif()
26-
27-
add_library(rac_backend_metalrt STATIC ${METALRT_SOURCES})
35+
# Folded into rac_commons as an OBJECT library — no separate artifact to ship.
36+
add_library(rac_backend_metalrt OBJECT ${METALRT_WRAPPER_SOURCES})
2837

2938
target_include_directories(rac_backend_metalrt PRIVATE
3039
${CMAKE_SOURCE_DIR}/include
3140
${CMAKE_CURRENT_SOURCE_DIR}
32-
${METALRT_INCLUDE_DIR}
3341
)
3442

3543
target_compile_definitions(rac_backend_metalrt PRIVATE RAC_METALRT_BUILDING)
3644

37-
# Link MetalRT static library
38-
# NO_CMAKE_FIND_ROOT_PATH: the engine is built locally, not inside the iOS SDK sysroot
39-
find_library(METALRT_ENGINE_LIB
40-
NAMES metalrt_engine
41-
PATHS ${_metalrt_lib_dir}
42-
NO_DEFAULT_PATH
43-
NO_CMAKE_FIND_ROOT_PATH
44-
)
45+
if(RAC_METALRT_ENGINE_AVAILABLE)
46+
# ------------------------------------------------------------------
47+
# Real engine path — MetalRT project must be present and built.
48+
# ------------------------------------------------------------------
49+
if(NOT DEFINED METALRT_ROOT)
50+
# Default: assume MetalRT is sibling to runanywhere-sdks
51+
set(METALRT_ROOT "${CMAKE_SOURCE_DIR}/../../../MetalRT" CACHE PATH "Path to MetalRT project root")
52+
endif()
53+
54+
set(METALRT_INCLUDE_DIR "${METALRT_ROOT}/src" CACHE PATH "Path to metalrt_c_api.h")
55+
56+
if(DEFINED METALRT_LIB_DIR)
57+
set(_metalrt_lib_dir "${METALRT_LIB_DIR}")
58+
else()
59+
set(_metalrt_lib_dir "${METALRT_ROOT}/build")
60+
endif()
61+
62+
# NO_CMAKE_FIND_ROOT_PATH: the engine is built locally, not inside the iOS sysroot.
63+
find_library(METALRT_ENGINE_LIB
64+
NAMES metalrt_engine
65+
PATHS ${_metalrt_lib_dir}
66+
NO_DEFAULT_PATH
67+
NO_CMAKE_FIND_ROOT_PATH
68+
REQUIRED
69+
)
70+
71+
message(STATUS "MetalRT: engine AVAILABLE, linking ${METALRT_ENGINE_LIB}")
72+
target_include_directories(rac_backend_metalrt PRIVATE ${METALRT_INCLUDE_DIR})
73+
target_compile_definitions(rac_backend_metalrt PRIVATE RAC_METALRT_ENGINE_AVAILABLE=1)
4574

46-
if(METALRT_ENGINE_LIB)
47-
target_link_libraries(rac_backend_metalrt PRIVATE ${METALRT_ENGINE_LIB})
48-
message(STATUS "MetalRT: Found libmetalrt_engine at ${METALRT_ENGINE_LIB}")
75+
# The parent CMakeLists folds this target's objects into rac_commons; the
76+
# engine .a needs to be linked onto rac_commons from the parent scope so
77+
# that the OBJECT target's undefined symbols resolve in the final archive.
78+
set(RAC_METALRT_ENGINE_LIB ${METALRT_ENGINE_LIB} PARENT_SCOPE)
4979
else()
50-
message(WARNING "MetalRT: libmetalrt_engine.a not found in ${_metalrt_lib_dir} — will need to be linked by the consuming app")
80+
# ------------------------------------------------------------------
81+
# Stub path — public-repo default. Compile the stub .c file so every
82+
# metalrt_* symbol resolves to a no-op. No external dependency.
83+
# ------------------------------------------------------------------
84+
message(STATUS "MetalRT: engine not available — compiling stubs")
85+
target_sources(rac_backend_metalrt PRIVATE stubs/metalrt_c_api_stub.c)
86+
target_include_directories(rac_backend_metalrt PRIVATE stubs)
87+
target_compile_definitions(rac_backend_metalrt PRIVATE RAC_METALRT_ENGINE_AVAILABLE=0)
5188
endif()
5289

53-
# Apple frameworks
90+
# Apple frameworks are always needed on this target (the wrappers reference
91+
# Metal-framework types in some paths even without the engine).
5492
target_link_libraries(rac_backend_metalrt PRIVATE
5593
"-framework Metal"
5694
"-framework Foundation"

sdk/runanywhere-commons/src/backends/metalrt/rac_backend_metalrt_register.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,15 @@ MetalRTRegistryState& get_state() {
337337
rac_bool_t metalrt_can_handle(const rac_service_request_t* request, void* /*user_data*/) {
338338
if (!request) return RAC_FALSE;
339339

340+
#if !defined(RAC_METALRT_ENGINE_AVAILABLE) || RAC_METALRT_ENGINE_AVAILABLE == 0
341+
// Stub build: the private MetalRT engine binary is not linked. Refuse to
342+
// handle any request so the service registry surfaces BACKEND_NOT_FOUND
343+
// at the loadModel call site instead of silently dispatching to stubs.
344+
(void)request;
345+
RAC_LOG_DEBUG(LOG_CAT,
346+
"can_handle: NO (MetalRT engine not available — stub build)");
347+
return RAC_FALSE;
348+
#else
340349
if (request->framework == RAC_FRAMEWORK_METALRT) {
341350
RAC_LOG_DEBUG(LOG_CAT, "can_handle: YES (framework=METALRT)");
342351
return RAC_TRUE;
@@ -345,6 +354,7 @@ rac_bool_t metalrt_can_handle(const rac_service_request_t* request, void* /*user
345354
RAC_LOG_DEBUG(LOG_CAT, "can_handle: NO (framework=%d, want METALRT=%d)",
346355
static_cast<int>(request->framework), RAC_FRAMEWORK_METALRT);
347356
return RAC_FALSE;
357+
#endif
348358
}
349359

350360
// =============================================================================
@@ -488,6 +498,18 @@ rac_result_t rac_backend_metalrt_register(void) {
488498
return RAC_ERROR_MODULE_ALREADY_REGISTERED;
489499
}
490500

501+
#if !defined(RAC_METALRT_ENGINE_AVAILABLE) || RAC_METALRT_ENGINE_AVAILABLE == 0
502+
// Stub build: the private MetalRT engine binary is not linked. Log clearly
503+
// and skip provider registration entirely so the registry never dispatches
504+
// a model load into no-op stubs.
505+
RAC_LOG_WARNING(LOG_CAT,
506+
"MetalRT backend compiled without engine binary — skipping "
507+
"provider registration. loadModel(..., framework: .metalrt) "
508+
"will fail with BACKEND_NOT_FOUND until the engine is installed.");
509+
state.registered = true;
510+
return RAC_SUCCESS;
511+
#endif
512+
491513
// Register module
492514
rac_module_info_t module_info = {};
493515
module_info.id = state.module_id;

0 commit comments

Comments
 (0)