Packages

iOS device USB interface discovery for Elixir

Current section

Files

Jump to
ifone c_src ifone_nif.c
Raw

c_src/ifone_nif.c

// ifone_nif.c - IOKit NIF for querying USB NCM interfaces on macOS
#include <erl_nif.h>
#include <string.h>
#include <stdio.h>
// Pre-created atoms (initialized in nif_load)
static ERL_NIF_TERM ATOM_OK;
static ERL_NIF_TERM ATOM_ERROR;
static ERL_NIF_TERM ATOM_NIL;
static ERL_NIF_TERM ATOM_ENOMEM;
static ERL_NIF_TERM ATOM_CONFIGURATION;
static ERL_NIF_TERM ATOM_INTERFACES;
static ERL_NIF_TERM ATOM_MATCHING_FAILED;
static ERL_NIF_TERM ATOM_SERVICE_MATCHING_FAILED;
static ERL_NIF_TERM ATOM_NOT_FOUND;
static int nif_load(ErlNifEnv *env, void **priv_data, ERL_NIF_TERM load_info) {
(void)priv_data;
(void)load_info;
ATOM_OK = enif_make_atom(env, "ok");
ATOM_ERROR = enif_make_atom(env, "error");
ATOM_NIL = enif_make_atom(env, "nil");
ATOM_ENOMEM = enif_make_atom(env, "enomem");
ATOM_CONFIGURATION = enif_make_atom(env, "configuration");
ATOM_INTERFACES = enif_make_atom(env, "interfaces");
ATOM_MATCHING_FAILED = enif_make_atom(env, "matching_failed");
ATOM_SERVICE_MATCHING_FAILED = enif_make_atom(env, "service_matching_failed");
ATOM_NOT_FOUND = enif_make_atom(env, "not_found");
return 0;
}
#ifdef __APPLE__
#include <IOKit/IOKitLib.h>
#include <IOKit/usb/IOUSBLib.h>
#include <IOKit/network/IONetworkInterface.h>
#include <CoreFoundation/CoreFoundation.h>
// Apple's USB vendor ID.
#define APPLE_VENDOR_ID 0x05AC
// USB CDC-Data interface class (the NCM data interface on iOS devices).
#define CDC_DATA_CLASS 10
// Helper to convert CFString to C string
static int cf_string_to_c(CFStringRef cf_str, char *buffer, size_t buffer_size) {
if (!cf_str) return 0;
return CFStringGetCString(cf_str, buffer, buffer_size, kCFStringEncodingUTF8);
}
// Helper to create an Elixir binary from a C string
static ERL_NIF_TERM make_binary_string(ErlNifEnv *env, const char *str) {
if (!str) {
return enif_make_badarg(env);
}
size_t len = strlen(str);
ERL_NIF_TERM bin;
unsigned char *data = enif_make_new_binary(env, len, &bin);
if (!data) {
return enif_raise_exception(env, ATOM_ENOMEM);
}
memcpy(data, str, len);
return bin;
}
// Helper to get a string property from an IOKit object
static int get_string_property(io_object_t obj, CFStringRef key, char *buffer, size_t buffer_size) {
CFTypeRef value = IORegistryEntryCreateCFProperty(obj, key, kCFAllocatorDefault, 0);
if (!value) return 0;
int result = 0;
if (CFGetTypeID(value) == CFStringGetTypeID()) {
result = cf_string_to_c((CFStringRef)value, buffer, buffer_size);
}
CFRelease(value);
return result;
}
// Helper to get an integer property from an IOKit object
static int get_int_property(io_object_t obj, CFStringRef key, int64_t *out_value) {
CFTypeRef value = IORegistryEntryCreateCFProperty(obj, key, kCFAllocatorDefault, 0);
if (!value) return 0;
int result = 0;
if (CFGetTypeID(value) == CFNumberGetTypeID()) {
result = CFNumberGetValue((CFNumberRef)value, kCFNumberSInt64Type, out_value);
}
CFRelease(value);
return result;
}
// Get the USB serial number for a network interface
static int get_serial_for_network_interface(const char *if_name, char *serial_out, size_t serial_size) {
io_iterator_t iterator;
io_object_t interface;
kern_return_t kr;
int found = 0;
// Find the IOEthernetInterface with matching BSD Name
CFMutableDictionaryRef matching = IOServiceMatching("IOEthernetInterface");
if (!matching) return 0;
kr = IOServiceGetMatchingServices(kIOMainPortDefault, matching, &iterator);
if (kr != KERN_SUCCESS) return 0;
while ((interface = IOIteratorNext(iterator)) != 0) {
char bsd_name[64];
if (get_string_property(interface, CFSTR("BSD Name"), bsd_name, sizeof(bsd_name))) {
if (strcmp(bsd_name, if_name) == 0) {
// Found the interface, now walk up to find USB device
io_object_t parent = 0;
io_object_t current = interface;
IOObjectRetain(current);
while (current) {
kr = IORegistryEntryGetParentEntry(current, kIOServicePlane, &parent);
IOObjectRelease(current);
if (kr != KERN_SUCCESS) break;
current = parent;
if (IOObjectConformsTo(current, "IOUSBHostDevice")) {
found = get_string_property(current, CFSTR("USB Serial Number"),
serial_out, serial_size);
IOObjectRelease(current);
break;
}
}
}
}
IOObjectRelease(interface);
if (found) break;
}
IOObjectRelease(iterator);
return found;
}
// Find a USB device by serial number or location ID
// Returns the device object (caller must release) or 0 if not found
static io_object_t find_usb_device(const char *serial, uint32_t location_id, int use_location) {
io_iterator_t iterator;
io_object_t device;
kern_return_t kr;
CFMutableDictionaryRef matching = IOServiceMatching("IOUSBHostDevice");
if (!matching) return 0;
kr = IOServiceGetMatchingServices(kIOMainPortDefault, matching, &iterator);
if (kr != KERN_SUCCESS) return 0;
while ((device = IOIteratorNext(iterator)) != 0) {
if (use_location) {
int64_t loc;
if (get_int_property(device, CFSTR("locationID"), &loc)) {
if ((uint32_t)loc == location_id) {
IOObjectRelease(iterator);
return device;
}
}
} else {
char dev_serial[256];
if (get_string_property(device, CFSTR("USB Serial Number"), dev_serial, sizeof(dev_serial))) {
if (strcmp(dev_serial, serial) == 0) {
IOObjectRelease(iterator);
return device;
}
}
}
IOObjectRelease(device);
}
IOObjectRelease(iterator);
return 0;
}
// Find the BSD name of the IOEthernetInterface bound under a CDC-Data interface.
// The registry layout is: interface -> AppleUSBNCMData -> IOEthernetInterface.
// Writes "" into buffer if no network interface is bound yet.
static void find_ethernet_bsd_name(io_object_t interface, char *buffer, size_t buffer_size) {
io_iterator_t child_iterator;
io_object_t child;
buffer[0] = '\0';
if (IORegistryEntryGetChildIterator(interface, kIOServicePlane, &child_iterator) != KERN_SUCCESS) {
return;
}
while ((child = IOIteratorNext(child_iterator)) != 0) {
io_iterator_t grandchild_iterator;
io_object_t grandchild;
if (IORegistryEntryGetChildIterator(child, kIOServicePlane, &grandchild_iterator) == KERN_SUCCESS) {
while ((grandchild = IOIteratorNext(grandchild_iterator)) != 0) {
if (IOObjectConformsTo(grandchild, "IOEthernetInterface")) {
get_string_property(grandchild, CFSTR("BSD Name"), buffer, buffer_size);
}
IOObjectRelease(grandchild);
if (buffer[0]) break;
}
IOObjectRelease(grandchild_iterator);
}
IOObjectRelease(child);
if (buffer[0]) break;
}
IOObjectRelease(child_iterator);
}
// Build the %{interface_number => bsd_name | nil} map for a device by scanning
// its direct IOUSBHostInterface children for the CDC-Data class. When count_out
// is non-NULL, it receives the number of NCM data interfaces found.
static ERL_NIF_TERM build_ncm_interfaces_map(ErlNifEnv *env, io_object_t device, int *count_out) {
ERL_NIF_TERM interfaces_map = enif_make_new_map(env);
io_iterator_t child_iterator;
io_object_t interface;
int count = 0;
if (IORegistryEntryGetChildIterator(device, kIOServicePlane, &child_iterator) == KERN_SUCCESS) {
while ((interface = IOIteratorNext(child_iterator)) != 0) {
int64_t interface_class = 0;
if (IOObjectConformsTo(interface, "IOUSBHostInterface") &&
get_int_property(interface, CFSTR("bInterfaceClass"), &interface_class) &&
interface_class == CDC_DATA_CLASS) {
int64_t interface_number = 0;
get_int_property(interface, CFSTR("bInterfaceNumber"), &interface_number);
char network_if_name[64];
find_ethernet_bsd_name(interface, network_if_name, sizeof(network_if_name));
ERL_NIF_TERM if_num_term = enif_make_int(env, (int)interface_number);
ERL_NIF_TERM if_name_term = network_if_name[0]
? make_binary_string(env, network_if_name)
: ATOM_NIL;
enif_make_map_put(env, interfaces_map, if_num_term, if_name_term, &interfaces_map);
count++;
}
IOObjectRelease(interface);
}
IOObjectRelease(child_iterator);
}
if (count_out) *count_out = count;
return interfaces_map;
}
// Assemble the per-device result: %{configuration: N, interfaces: %{...}}
static ERL_NIF_TERM make_device_result(ErlNifEnv *env, io_object_t device, ERL_NIF_TERM interfaces_map) {
int64_t configuration = 0;
get_int_property(device, CFSTR("kUSBCurrentConfiguration"), &configuration);
ERL_NIF_TERM keys[] = {ATOM_CONFIGURATION, ATOM_INTERFACES};
ERL_NIF_TERM values[] = {enif_make_int(env, (int)configuration), interfaces_map};
ERL_NIF_TERM result_map;
enif_make_map_from_arrays(env, keys, values, 2, &result_map);
return result_map;
}
// Whether the device reports Apple's USB vendor ID.
static int is_apple_vendor(io_object_t device) {
int64_t vendor_id = 0;
return get_int_property(device, CFSTR("idVendor"), &vendor_id) && vendor_id == APPLE_VENDOR_ID;
}
// NIF: list_ncm_data_interfaces(serial_or_location)
// Returns: {:ok, %{configuration: N, interfaces: %{num => name, ...}}} | {:error, reason}
static ERL_NIF_TERM list_ncm_data_interfaces_nif(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
char identifier[256];
uint32_t location_id = 0;
int use_location = 0;
if (argc != 1) {
return enif_make_badarg(env);
}
// Check if it's an integer (location ID) or binary (serial)
ErlNifBinary bin;
if (enif_get_uint(env, argv[0], &location_id)) {
use_location = 1;
} else if (enif_inspect_binary(env, argv[0], &bin)) {
if (bin.size >= sizeof(identifier)) {
return enif_make_badarg(env);
}
memcpy(identifier, bin.data, bin.size);
identifier[bin.size] = '\0';
} else {
return enif_make_badarg(env);
}
io_object_t usb_device = find_usb_device(identifier, location_id, use_location);
if (!usb_device) {
return enif_make_tuple2(env, ATOM_ERROR, ATOM_NOT_FOUND);
}
ERL_NIF_TERM interfaces_map = build_ncm_interfaces_map(env, usb_device, NULL);
ERL_NIF_TERM result_map = make_device_result(env, usb_device, interfaces_map);
IOObjectRelease(usb_device);
return enif_make_tuple2(env, ATOM_OK, result_map);
}
// NIF: list_devices()
// Enumerates all connected iOS devices and their NCM interfaces in one pass.
// Returns: {:ok, %{serial => %{configuration: N, interfaces: %{...}}}} | {:error, reason}
static ERL_NIF_TERM list_devices_nif(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argv;
if (argc != 0) {
return enif_make_badarg(env);
}
CFMutableDictionaryRef matching = IOServiceMatching("IOUSBHostDevice");
if (!matching) {
return enif_make_tuple2(env, ATOM_ERROR, ATOM_MATCHING_FAILED);
}
io_iterator_t iterator;
if (IOServiceGetMatchingServices(kIOMainPortDefault, matching, &iterator) != KERN_SUCCESS) {
return enif_make_tuple2(env, ATOM_ERROR, ATOM_SERVICE_MATCHING_FAILED);
}
ERL_NIF_TERM devices_map = enif_make_new_map(env);
io_object_t device;
while ((device = IOIteratorNext(iterator)) != 0) {
if (is_apple_vendor(device)) {
char serial[256];
if (get_string_property(device, CFSTR("USB Serial Number"), serial, sizeof(serial))) {
int iface_count = 0;
ERL_NIF_TERM interfaces_map = build_ncm_interfaces_map(env, device, &iface_count);
// Only list Apple devices that expose at least one NCM interface, so
// peripherals like keyboards — and iOS devices that have not brought
// NCM up yet — are excluded. Mirrors the Linux behavior.
if (iface_count > 0) {
ERL_NIF_TERM serial_term = make_binary_string(env, serial);
ERL_NIF_TERM result_map = make_device_result(env, device, interfaces_map);
enif_make_map_put(env, devices_map, serial_term, result_map, &devices_map);
}
}
}
IOObjectRelease(device);
}
IOObjectRelease(iterator);
return enif_make_tuple2(env, ATOM_OK, devices_map);
}
// NIF: get_usb_serial_for_interface(interface_name)
// Returns: {:ok, serial} | {:error, :not_found}
static ERL_NIF_TERM get_usb_serial_for_interface_nif(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
char if_name[64];
char serial[256];
if (argc != 1) {
return enif_make_badarg(env);
}
ErlNifBinary bin;
if (!enif_inspect_binary(env, argv[0], &bin)) {
return enif_make_badarg(env);
}
if (bin.size >= sizeof(if_name)) {
return enif_make_badarg(env);
}
memcpy(if_name, bin.data, bin.size);
if_name[bin.size] = '\0';
if (get_serial_for_network_interface(if_name, serial, sizeof(serial))) {
return enif_make_tuple2(env, ATOM_OK, make_binary_string(env, serial));
}
return enif_make_tuple2(env, ATOM_ERROR, ATOM_NOT_FOUND);
}
#endif
#ifndef __APPLE__
// Empty NIF table for non-macOS - the NIF won't be loaded anyway
static ErlNifFunc nif_funcs[] = {};
#else
static ErlNifFunc nif_funcs[] = {
{"list_ncm_data_interfaces", 1, list_ncm_data_interfaces_nif, 0},
{"list_devices", 0, list_devices_nif, 0},
{"get_serial_for_interface", 1, get_usb_serial_for_interface_nif, 0}
};
#endif
ERL_NIF_INIT(Elixir.Ifone.USB.Darwin, nif_funcs, nif_load, NULL, NULL, NULL)