Current section
Files
Jump to
Current section
Files
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>
// 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;
}
// Find the parent USB device for an interface and check if serial matches
static int interface_matches_serial(io_object_t interface, const char *target_serial) {
io_object_t parent = 0;
kern_return_t kr;
char serial[256];
// Walk up to find the USB device parent
io_object_t current = interface;
IOObjectRetain(current);
while (current) {
kr = IORegistryEntryGetParentEntry(current, kIOServicePlane, &parent);
IOObjectRelease(current);
if (kr != KERN_SUCCESS) break;
current = parent;
// Check if this is an IOUSBHostDevice
if (IOObjectConformsTo(current, "IOUSBHostDevice")) {
if (get_string_property(current, CFSTR("USB Serial Number"), serial, sizeof(serial))) {
IOObjectRelease(current);
return strcmp(serial, target_serial) == 0;
}
}
}
return 0;
}
// Find the parent USB device for an interface by location ID
static int interface_matches_location(io_object_t interface, uint32_t target_location) {
io_object_t parent = 0;
kern_return_t kr;
int64_t location;
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")) {
if (get_int_property(current, CFSTR("locationID"), &location)) {
IOObjectRelease(current);
return (uint32_t)location == target_location;
}
}
}
return 0;
}
// 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;
}
// 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);
}
// Find the USB device first
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);
}
// Get the current configuration
int64_t configuration = 0;
get_int_property(usb_device, CFSTR("kUSBCurrentConfiguration"), &configuration);
IOObjectRelease(usb_device);
// Now find all matching interfaces
io_iterator_t iterator;
io_object_t interface;
kern_return_t kr;
ERL_NIF_TERM interfaces_map = enif_make_new_map(env);
CFMutableDictionaryRef matching = IOServiceMatching("IOUSBHostInterface");
if (!matching) {
return enif_make_tuple2(env, ATOM_ERROR, ATOM_MATCHING_FAILED);
}
kr = IOServiceGetMatchingServices(kIOMainPortDefault, matching, &iterator);
if (kr != KERN_SUCCESS) {
return enif_make_tuple2(env, ATOM_ERROR, ATOM_SERVICE_MATCHING_FAILED);
}
while ((interface = IOIteratorNext(iterator)) != 0) {
int64_t interface_class = 0;
int64_t interface_number = 0;
// Check if this is a CDC Data interface (class 0x0A = 10)
if (!get_int_property(interface, CFSTR("bInterfaceClass"), &interface_class) ||
interface_class != 10) {
IOObjectRelease(interface);
continue;
}
// Check if this interface belongs to our target device
int matches = 0;
if (use_location) {
matches = interface_matches_location(interface, location_id);
} else {
matches = interface_matches_serial(interface, identifier);
}
if (!matches) {
IOObjectRelease(interface);
continue;
}
// Get interface number
get_int_property(interface, CFSTR("bInterfaceNumber"), &interface_number);
// Find the network interface child (IOEthernetInterface)
char network_if_name[64] = "";
io_iterator_t child_iterator;
io_object_t child;
kr = IORegistryEntryGetChildIterator(interface, kIOServicePlane, &child_iterator);
if (kr == KERN_SUCCESS) {
while ((child = IOIteratorNext(child_iterator)) != 0) {
// Look for AppleUSBNCMData or similar, then its IOEthernetInterface child
io_iterator_t grandchild_iterator;
io_object_t grandchild;
kr = IORegistryEntryGetChildIterator(child, kIOServicePlane, &grandchild_iterator);
if (kr == KERN_SUCCESS) {
while ((grandchild = IOIteratorNext(grandchild_iterator)) != 0) {
if (IOObjectConformsTo(grandchild, "IOEthernetInterface")) {
get_string_property(grandchild, CFSTR("BSD Name"),
network_if_name, sizeof(network_if_name));
}
IOObjectRelease(grandchild);
if (network_if_name[0]) break;
}
IOObjectRelease(grandchild_iterator);
}
IOObjectRelease(child);
if (network_if_name[0]) break;
}
IOObjectRelease(child_iterator);
}
// Add to interfaces map: interface_number => network_if_name
ERL_NIF_TERM if_num_term = enif_make_int(env, (int)interface_number);
ERL_NIF_TERM if_name_term;
if (network_if_name[0]) {
if_name_term = make_binary_string(env, network_if_name);
} else {
if_name_term = ATOM_NIL;
}
enif_make_map_put(env, interfaces_map, if_num_term, if_name_term, &interfaces_map);
IOObjectRelease(interface);
}
IOObjectRelease(iterator);
// Build result map: %{configuration: N, interfaces: %{...}}
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 enif_make_tuple2(env, ATOM_OK, result_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},
{"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)