Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions .clang-tidy
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2026 Nirapod Labs

Checks: >
clang-analyzer-*,
bugprone-*,
performance-*,
readability-*
WarningsAsErrors: ''
#
# Curated for a low-level C wire codec and an interposer. The bug-finding
# families stay on (clang-analyzer, bugprone, performance). Off:
# readability-identifier-length one-letter loop/byte vars are idiomatic here
# readability-magic-numbers the CBOR byte constants are the wire format
# readability-braces-around-statements clashes with .clang-format compact returns
# bugprone-easily-swappable-parameters flags ordinary (buf,len)/(ptr,count) C
# ...insecureAPI.DeprecatedOrUnsafeBufferHandling wants C11 Annex K _s funcs, absent on Darwin
# Test and demo harnesses relax two more checks; see packages/*/tests/.clang-tidy.
Checks: "clang-analyzer-*,bugprone-*,-bugprone-easily-swappable-parameters,performance-*,readability-*,-readability-identifier-length,-readability-magic-numbers,-readability-braces-around-statements,-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling"
WarningsAsErrors: ""
HeaderFilterRegex: "packages/interpose/(src|include)/.*"
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# SPDX-FileCopyrightText: 2026 Nirapod Labs

cmake_minimum_required(VERSION 3.24)
project(SimBLE LANGUAGES C)
project(SimBLE LANGUAGES C OBJC)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
Expand Down
42 changes: 40 additions & 2 deletions packages/interpose/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,24 @@ else()
set(SIMBLE_INTERPOSE_OUTPUT "simble-interpose")
endif()

set(SIMBLE_FRAMEWORKS "-framework CoreBluetooth" "-framework Foundation")

# The interposer core: the central swizzles, the shadow registry, and the loopback transport.
# No load constructor here; the dylib and the host harness add their own entry point, so both
# reuse this. The .m files use ARC.
add_library(interpose_core STATIC
src/hooks/central_hooks.m
src/registry/shadow.m
src/transport/client.c
)
target_include_directories(interpose_core PUBLIC include src/transport src/registry)
target_compile_options(interpose_core PRIVATE $<$<COMPILE_LANGUAGE:OBJC>:-fobjc-arc>)
target_link_libraries(interpose_core PUBLIC simble_protocol_c ${SIMBLE_FRAMEWORKS})

if(SIMBLE_SIM_SLICE)
# The injectable dylib: the core plus the dyld load constructor. Each simulator platform gets
# its own name; ios keeps the canonical simble-interpose, so the helper and scripts that load
# it by that name need no change.
add_library(simble_interpose MODULE src/entry.c)
target_include_directories(simble_interpose PRIVATE include)
set_target_properties(
Expand All @@ -16,8 +33,29 @@ if(SIMBLE_SIM_SLICE)
PREFIX ""
OUTPUT_NAME "${SIMBLE_INTERPOSE_OUTPUT}"
)
target_link_libraries(simble_interpose PRIVATE interpose_core ${SIMBLE_FRAMEWORKS})
else()
add_executable(hook_smoke tests/hook_smoke.c src/entry.c)
target_include_directories(hook_smoke PRIVATE include)
# The swizzle smoke test: install and uninstall on the host, where the CB classes exist.
add_executable(hook_smoke tests/hook_smoke.c)
target_link_libraries(hook_smoke PRIVATE interpose_core ${SIMBLE_FRAMEWORKS})
add_test(NAME hook_smoke COMMAND hook_smoke)

# The shadow registry unit test: mint, look up, fail closed.
add_executable(shadow_registry_test tests/shadow_registry_test.m)
target_compile_options(shadow_registry_test PRIVATE -fobjc-arc)
target_link_libraries(shadow_registry_test PRIVATE interpose_core ${SIMBLE_FRAMEWORKS})
add_test(NAME shadow_registry COMMAND shadow_registry_test)

# The passthrough invariant: a non-managed CBCentralManager method is unaffected by the
# installed swizzle. No helper, so it runs as a plain ctest.
add_executable(passthrough_test tests/passthrough_test.m)
target_compile_options(passthrough_test PRIVATE -fobjc-arc)
target_link_libraries(passthrough_test PRIVATE interpose_core ${SIMBLE_FRAMEWORKS})
add_test(NAME passthrough COMMAND passthrough_test)

# The transport round-trip: the client encodes a request and decodes a response and an event
# against an in-test loopback echo, no radio and no helper.
add_executable(client_roundtrip tests/client_roundtrip.c)
target_link_libraries(client_roundtrip PRIVATE interpose_core ${SIMBLE_FRAMEWORKS})
add_test(NAME client_roundtrip COMMAND client_roundtrip)
endif()
10 changes: 7 additions & 3 deletions packages/interpose/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ SPDX-FileCopyrightText: 2026 Nirapod Labs

# interpose

The injected simulator dylib for CoreBluetooth routing.
The injected simulator dylib that routes a guest app's CoreBluetooth calls to the host.

The scaffold builds placeholder iOS and watchOS simulator slices. The
Objective-C runtime hooks and shadow object registries are not implemented yet.
It hooks the CoreBluetooth central surface with Objective-C runtime swizzling, holds a shadow
registry for the framework's opaque objects, and carries each operation to the helper over the
loopback transport, delivering host events back as the guest's delegate callbacks. Scan, connect,
read, write, notify, and RSSI are routed; service and characteristic discovery are not routed in the
central interposer, and the peripheral role is not implemented. The dylib is a simulator slice,
loaded only by a debug scheme and never by a shipped app (see `SECURITY.md` for the fence).
52 changes: 51 additions & 1 deletion packages/interpose/include/simble_interpose.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,16 @@

/**
* @file simble_interpose.h
* @brief Public entry points for the simulator interposer scaffold.
* @brief The injected interposer's entry points: swizzle installation and the
* route-fire counters.
*
* @details
* The dylib's constructor calls ::simble_install_hooks at load when the environment
* is configured (SIMBLE_PORT and SIMBLE_TOKEN present) and stays inert otherwise.
* Counters a host harness reads to confirm the swizzles fired.
*
* @author Nirapod Labs
* @date 2026
*/

#ifndef SIMBLE_INTERPOSE_H
Expand All @@ -15,9 +24,50 @@
extern "C" {
#endif

/**
* @defgroup simble_interpose Interposer entry points
* @brief Swizzle installation, uninstall, and the route counters.
* @{
*/

/** Return the interposer scaffold version. */
int simble_interpose_version(void);

/**
* @brief Install the CoreBluetooth central swizzles.
*
* Exchanges the CBCentralManager and CBPeripheral method implementations for the routed
* ones. Idempotent: a second call is a no-op while the swizzles are installed.
*
* @return The number of selectors that could not be swizzled; 0 means every one is in place.
*/
int simble_install_hooks(void);

/**
* @brief Remove the swizzles, restoring the original method implementations.
*
* Idempotent: a call while nothing is installed is a no-op.
*/
void simble_uninstall_hooks(void);

/** How many times each routed central operation has fired since install. */
typedef struct {
int scan_start; ///< scanForPeripheralsWithServices:options: calls routed to the helper.
int connect; ///< connectPeripheral:options: calls routed to the helper.
int read; ///< readValueForCharacteristic: calls routed to the helper.
int write; ///< writeValue:forCharacteristic:type: calls routed to the helper.
int set_notify; ///< setNotifyValue:forCharacteristic: calls routed to the helper.
} simble_hook_stats;

/**
* @brief Read the route-fire counters.
*
* @return A copy of the counters at the time of the call.
*/
simble_hook_stats simble_get_hook_stats(void);

/** @} */

#ifdef __cplusplus
}
#endif
Expand Down
29 changes: 27 additions & 2 deletions packages/interpose/src/entry.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,33 @@
* SPDX-FileCopyrightText: 2026 Nirapod Labs
*/

/**
* @file entry.c
* @brief The dylib constructor: installs the central swizzles at load, inert without
* configuration.
*
* @details
* dyld runs the constructor when the dylib loads, before the app's main, so the
* CoreBluetooth central swizzles are in place before any CBCentralManager call. Loaded
* only via the debug scheme's dyld insert list; a release build bundles no dylib at all,
* which is the fence, not this gate.
*
* @author Nirapod Labs
* @date 2026
*/

#include "simble_interpose.h"

int simble_interpose_version(void) { return 1; }
#include <stdio.h>
#include <stdlib.h>

__attribute__((constructor)) static void simble_interpose_init(void) {}
__attribute__((constructor)) static void simble_interpose_init(void) {
// Inert without the dev-scheme env (SIMBLE_PORT, SIMBLE_TOKEN). Not the fence; a release build
// bundles no dylib.
if (!getenv("SIMBLE_PORT") || !getenv("SIMBLE_TOKEN"))
return;
int failures = simble_install_hooks();
if (failures != 0) {
fprintf(stderr, "[simble] %d swizzle(s) failed to install\n", failures);
}
}
Loading
Loading