How vphone-cli Handles HID Injection in the Guest Daemon: A Deep Dive into macOS-to-iOS Event Routing
vphone-cli handles HID injection by sending JSON-encoded commands from the host over vsock to the guest vphoned daemon, which dynamically binds Apple's private IOKit HID APIs at runtime to synthesize keyboard and touch events.
The vphone-cli project provides a robust mechanism for injecting human interface device events into iOS virtual machines running on Apple Silicon Macs. Unlike the standard Virtualization.framework USB multitouch path—which fails on iOS 18 base kernels—this approach uses a custom vsock-based protocol and guest-side daemon to deliver events directly through the iOS HID subsystem. This article examines how vphone-cli performs HID injection in the guest daemon, tracing the complete flow from host Swift code to guest Objective-C implementation.
Host-Side Message Preparation
The injection process begins in sources/vphone-cli/VPhoneControl.swift, where the host constructs structured HID requests.
Keyboard and Button Events
The VPhoneControl class exposes sendHID(page:usage:down:) for raw HID access and a convenience wrapper sendHIDPress(page:usage:) for single keypresses:
// VPhoneControl.swift
func sendHIDPress(page: UInt32, usage: UInt32) {
sendHID(page: page, usage: usage, down: nil) // "down" omitted → press-tap
}
This serializes a JSON payload containing protocol version, request type "hid", unique request ID, HID page, usage code, and optional down boolean.
Touch Events
Digitizer injection uses sendTouch(phase:x:y:) with normalized coordinates (0.0–1.0) and phase constants (0=down, 1=move, 3=up):
control.sendTouch(phase: 0, x: 0.5, y: 0.5) // touch down at screen center
control.sendTouch(phase: 3, x: 0.5, y: 0.5) // touch up
vsock Transport Protocol
Events traverse the VM boundary via a length-prefixed JSON protocol implemented in scripts/vphoned/vphoned_protocol.m. The vp_read_message and vp_write_message functions handle complete message framing, ensuring atomic delivery of variable-length commands.
Guest Daemon Architecture: vphoned_hid.m
The core HID injection logic resides in scripts/vphoned/vphoned_hid.m. This module dynamically loads Apple's private IOKit framework to avoid compile-time linkage against unpublished APIs.
Dynamic IOKit Symbol Resolution
The vp_hid_load() function implements lazy binding on first use:
static int vp_hid_load(void) {
// dlopen IOKit.framework and resolve:
// - IOHIDEventCreateKeyboardEvent
// - IOHIDEventCreateDigitizerEvent
// - IOHIDEventCreateDigitizerFingerEvent
// - IOHIDEventSystemClientDispatchEvent
}
If digitizer symbols are unavailable, touch injection degrades gracefully by returning early from touch functions.
Serial Event Dispatch Queue
All HID operations execute on a dedicated serial queue created during initialization:
gHIDQueue = dispatch_queue_create("com.vphone.hid",
DISPATCH_QUEUE_SERIAL);
dispatch_set_qos_class_fallback(gHIDQueue, QOS_CLASS_USER_INTERACTIVE);
The QOS_CLASS_USER_INTERACTIVE quality of service level guarantees ordered, low-latency event delivery critical for responsive input.
Keyboard Event Injection Flow
When vphoned receives a "hid" message, it dispatches to vp_hid_key (stateful press/release) or vp_hid_press (automatic tap):
// vphoned_hid.m
void vp_hid_press(uint32_t page, uint32_t usage) {
IOHIDEventRef down = pKeyboard(kCFAllocatorDefault,
mach_absolute_time(),
page, usage, 1, 0); // key down
send_hid_event(down);
usleep(100000); // 100ms hold duration
IOHIDEventRef up = pKeyboard(kCFAllocatorDefault,
mach_absolute_time(),
page, usage, 0, 0); // key up
send_hid_event(up);
}
The send_hid_event helper sets a fixed sender ID (0x8000000817319372) and posts to IOHIDEventSystemClientDispatchEvent on gHIDQueue.
Touch Event Injection: Digitizer Construction
Touch events require building a composite event structure. The vp_hid_touch function translates phase codes into digitizer operations:
// vphoned_hid.m
void vp_hid_touch(int phase, double x, double y) {
switch (phase) {
case 0: // down
dispatch_digitizer(x, y, 1, 1,
VP_DIG_TOUCH | VP_DIG_IDENTITY);
break;
case 1: // move
dispatch_digitizer(x, y, 1, 1, VP_DIG_POSITION);
break;
case 3: // up
default:
dispatch_digitizer(x, y, 0, 0,
VP_DIG_TOUCH | VP_DIG_IDENTITY);
break;
}
}
The dispatch_digitizer function constructs a hierarchy of events:
- Parent digitizer event via
IOHIDEventCreateDigitizerEvent - Finger sub-event via
IOHIDEventCreateDigitizerFingerEvent - Display integration flag applied via
IOHIDEventSetIntegerValuewith keykIOHIDEventFieldDigitizerIsDisplayIntegrated - Append finger to parent and dispatch combined structure
This multi-level construction mirrors physical touchscreen hardware reporting, ensuring iOS recognizes the input as genuine display touch data.
Capability Negotiation at Connection
The host determines injection availability through an initial handshake. In VPhoneControl.performHandshake, the guest advertises capabilities via JSON:
{"caps":["hid","touch"],"iosVersion":"18.1"}
The host evaluates guestCaps.contains("touch") and guestIOSVersion to set useGuestTouchInjection, falling back to alternative input methods when the daemon lacks support.
Key Implementation Files
| File Path | Role in HID Injection |
|---|---|
sources/vphone-cli/VPhoneControl.swift |
Host vsock client, JSON message construction, handshake management |
scripts/vphoned/vphoned_hid.m |
Guest daemon HID implementation, dynamic IOKit binding, event creation and dispatch |
scripts/vphoned/vphoned_protocol.m |
Length-prefixed JSON protocol for reliable host-guest communication |
sources/vphone-cli/VPhoneMenuKeys.swift |
UI translation layer mapping system actions (Home, Power) to HID codes |
sources/vphone-cli/VPhoneKeyHelper.swift |
macOS key event to HID usage mapping for keyboard forwarding |
Summary
- vphone-cli solves iOS VM input limitations by implementing guest-side HID injection through the private IOKit framework
- Dynamic symbol loading at runtime avoids linking against unpublished APIs while maintaining forward compatibility
- Structured JSON protocol over vsock provides reliable, ordered message delivery between host and guest
- Dedicated serial queue with interactive QoS class ensures low-latency, deterministic event timing
- Composite digitizer events faithfully replicate physical touchscreen hardware for seamless iOS integration
- Capability negotiation enables graceful degradation when guest features are unavailable
Frequently Asked Questions
How does vphone-cli differ from standard Virtualization.framework touch input?
Standard Virtualization.framework relies on VZUSBKeyboard and VZUSBMultiTouch configuration, which Apple disabled for iOS 18 base kernels. vphone-cli bypasses this limitation by connecting directly to the iOS HID event system via private IOKit APIs inside the guest, making touch injection possible where native USB multitouch fails.
Why use dynamic loading instead of linking against IOKit directly?
Apple's HID event creation functions are private APIs not exposed in public headers. Dynamic loading via dlopen and dlsym allows vphone-cli's guest daemon to compile without unauthorized framework linkages, and to degrade gracefully—silently disabling touch features if symbols are absent—rather than crashing at launch on newer or modified iOS versions.
What is the purpose of the fixed sender ID in HID events?
The sender ID 0x8000000817319372 identifies the virtual input source to the iOS HID system, distinguishing injected events from physical hardware. Consistent sender assignment enables iOS to track input provenance and maintain proper event attribution across the window server and application layers.
Can vphone-cli inject multi-touch gestures?
The current implementation supports single-finger touch events through the vp_hid_touch interface. The dispatch_digitizer architecture accommodates extension—multiple finger sub-events can be appended to the parent digitizer event—but the host protocol and convenience methods in VPhoneControl currently expose only single-point control.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →