From 3d97c8fc9d769eab64642185c7ef122caae32dbf Mon Sep 17 00:00:00 2001 From: James Moschou Date: Fri, 14 Aug 2026 15:43:30 +0200 Subject: [PATCH 1/5] Add util::adopt_objc to create an objc_ptr that is already +1 retained --- .../Utilities/include/Utilities/ObjCPointer.h | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Sources/Utilities/include/Utilities/ObjCPointer.h b/Sources/Utilities/include/Utilities/ObjCPointer.h index adde954..5744ec7 100644 --- a/Sources/Utilities/include/Utilities/ObjCPointer.h +++ b/Sources/Utilities/include/Utilities/ObjCPointer.h @@ -1,8 +1,7 @@ #pragma once -#ifdef __OBJC__ - #include +#include #include @@ -16,6 +15,11 @@ OBJC_EXPORT void objc_release(id obj); namespace util { +template +class objc_ptr; +template +objc_ptr adopt_objc(T obj) noexcept; + template class objc_ptr { private: @@ -24,6 +28,11 @@ class objc_ptr { static inline id to_storage(T obj) { return (id)(obj); } static inline T from_storage(id storage) { return (T)storage; } + enum AdoptTag { Adopt }; + constexpr objc_ptr(T obj, AdoptTag) : _storage(to_storage(obj)) {} + + friend objc_ptr adopt_objc(T obj) noexcept; + public: constexpr objc_ptr() noexcept : _storage(nullptr) {} constexpr objc_ptr(std::nullptr_t) noexcept : _storage(nullptr) {} @@ -101,8 +110,11 @@ class objc_ptr { explicit operator bool() const noexcept { return _storage != nullptr; } }; +template +objc_ptr adopt_objc(T obj) noexcept { + return objc_ptr(obj, objc_ptr::Adopt); +} + } // namespace util UTIL_ASSUME_NONNULL_END - -#endif From 111f4c748ba5d672b6934d87751f69f950774ffd Mon Sep 17 00:00:00 2001 From: James Moschou Date: Fri, 14 Aug 2026 15:44:00 +0200 Subject: [PATCH 2/5] Move calls to objc_release to end of method code blocks --- .../Utilities/include/Utilities/ObjCPointer.h | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/Sources/Utilities/include/Utilities/ObjCPointer.h b/Sources/Utilities/include/Utilities/ObjCPointer.h index 5744ec7..22c7e5c 100644 --- a/Sources/Utilities/include/Utilities/ObjCPointer.h +++ b/Sources/Utilities/include/Utilities/ObjCPointer.h @@ -64,24 +64,26 @@ class objc_ptr { objc_ptr &operator=(const objc_ptr &other) noexcept { if (this != &other) { - auto tmp = other._storage; - if (tmp) { - objc_retain(tmp); + id new_storage = other._storage; + if (new_storage) { + objc_retain(new_storage); } - if (_storage) { - objc_release(_storage); + id old_storage = _storage; + _storage = new_storage; + if (old_storage) { + objc_release(old_storage); } - _storage = tmp; } return *this; } objc_ptr &operator=(objc_ptr &&other) noexcept { if (this != &other) { - if (_storage) { - objc_release(_storage); - } + id old_storage = _storage; _storage = std::exchange(other._storage, nullptr); + if (old_storage) { + objc_release(old_storage); + } } return *this; } @@ -92,14 +94,15 @@ class objc_ptr { void reset(T obj = nullptr) noexcept { if (_storage != obj) { - auto tmp = obj; - if (tmp) { - objc_retain(tmp); + id new_obj = obj; + if (new_obj) { + objc_retain(new_obj); } - if (_storage) { - objc_release(_storage); + id old_storage = _storage; + _storage = new_obj; + if (old_storage) { + objc_release(old_storage); } - _storage = tmp; } } From a0eeb8132b1ba691d57f442d7f362a2c3b1d67f5 Mon Sep 17 00:00:00 2001 From: James Moschou Date: Fri, 14 Aug 2026 15:54:46 +0200 Subject: [PATCH 3/5] Add debug server implementation --- Sources/ComputeCxx/Debug/Connection.cpp | 145 ++++++++++++ Sources/ComputeCxx/Debug/DebugServer.cpp | 212 ++++++++++++++++++ Sources/ComputeCxx/Debug/DebugServer.h | 81 +++++++ Sources/ComputeCxx/Debug/DebugServer.mm | 82 +++++++ Sources/ComputeCxx/Debug/IAGDebugServer.cpp | 25 +++ Sources/ComputeCxx/Graph/Graph.cpp | 7 + .../include/ComputeCxx/ComputeCxx.h | 1 + .../include/ComputeCxx/IAGDebugServer.h | 49 ++++ 8 files changed, 602 insertions(+) create mode 100644 Sources/ComputeCxx/Debug/Connection.cpp create mode 100644 Sources/ComputeCxx/Debug/DebugServer.cpp create mode 100644 Sources/ComputeCxx/Debug/DebugServer.h create mode 100644 Sources/ComputeCxx/Debug/DebugServer.mm create mode 100644 Sources/ComputeCxx/Debug/IAGDebugServer.cpp create mode 100644 Sources/ComputeCxx/include/ComputeCxx/IAGDebugServer.h diff --git a/Sources/ComputeCxx/Debug/Connection.cpp b/Sources/ComputeCxx/Debug/Connection.cpp new file mode 100644 index 0000000..5bcc729 --- /dev/null +++ b/Sources/ComputeCxx/Debug/Connection.cpp @@ -0,0 +1,145 @@ +#include "DebugServer.h" + +#include + +#include + +namespace IAG { + +DebugServer::Connection::Connection(DebugServer *server, int socket) : _server(server), _socket(socket) { + dispatch_source_t event_source = + dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket, 0, dispatch_get_main_queue()); + dispatch_set_context(event_source, this); + dispatch_source_set_event_handler_f(event_source, handler); + dispatch_resume(event_source); + _event_source = util::adopt_objc(event_source); +} + +DebugServer::Connection::~Connection() { + dispatch_source_set_event_handler_f(_event_source.get(), nullptr); + dispatch_set_context(_event_source.get(), nullptr); + close(_socket); +} + +namespace { + +bool blocking_read(int socket_fd, void *buffer, size_t size) { + if (size == 0) { + return true; + } + + size_t total_read = 0; + char *bytes = static_cast(buffer); + + while (total_read < size) { + ssize_t bytes_read = read(socket_fd, bytes + total_read, size - total_read); + + if (bytes_read > 0) { + total_read += bytes_read; + } else if (bytes_read == 0) { + // Socket closed + return false; + } else { + if (errno == EINTR) { + continue; // Interrupted, retry + } +#if DEBUG + assert(errno != EAGAIN && errno != EWOULDBLOCK); // blocking mode shouldn't encounter these errors +#else + if (errno == EAGAIN || errno == EWOULDBLOCK) { + // Non-blocking mode: No data available, retry or handle accordingly + continue; + } +#endif + perror("IAGDebugServer: read"); + return false; + } + } + + return true; +} + +bool blocking_write(int socket_fd, const void *buffer, size_t size) { + if (size == 0) { + return true; + } + + size_t total_written = 0; + const char *bytes = static_cast(buffer); + + while (total_written < size) { + ssize_t bytes_written = write(socket_fd, bytes + total_written, size - total_written); + + if (bytes_written > 0) { + total_written += bytes_written; + } else if (bytes_written == 0) { + return false; // Unexpected write failure + } else { + if (errno == EINTR) { + continue; // Interrupted, retry + } else if (errno == EAGAIN || errno == EWOULDBLOCK) { + continue; // Non-blocking mode: Retry or handle accordingly + } else { + perror("IAGDebugServer: write"); + return false; + } + } + } + + return true; +} + +} // namespace + +void DebugServer::Connection::handler(void *context) { + Connection *connection = reinterpret_cast(context); + + uint8_t header_bytes[sizeof(IAGDebugServerMessageHeader)]; + if (!blocking_read(connection->_socket, header_bytes, sizeof(header_bytes))) { + connection->_server->close_connection(connection); + return; + } + + IAGDebugServerMessageHeader *header = reinterpret_cast(header_bytes); + if (header->token != connection->_server->_token) { + connection->_server->close_connection(connection); + return; + } + + CFIndex length = header->body_length; + CFMutableDataRef request_data = CFDataCreateMutable(kCFAllocatorDefault, length); + if (!request_data) { + connection->_server->close_connection(connection); + return; + } + + CFDataSetLength(request_data, length); + void *request_bytes = CFDataGetMutableBytePtr(request_data); + + if (blocking_read(connection->_socket, request_bytes, length)) { + CFDataRef response_data = connection->_server->receive(connection, header, request_data); + if (response_data) { + CFIndex response_length = CFDataGetLength(response_data); + if (response_length >> 32 == 0) { + header->body_length = (uint32_t)response_length; + if (blocking_write(connection->_socket, reinterpret_cast(header), + sizeof(IAGDebugServerMessageHeader))) { + const unsigned char *response_bytes = CFDataGetBytePtr(response_data); + if (blocking_write(connection->_socket, response_bytes, response_length)) { + connection = nullptr; // do not close connection + } + } + } + + CFRelease(response_data); + } + } + + CFRelease(request_data); + + if (connection) { + connection->_server->close_connection(connection); + } +} + +} // namespace IAG diff --git a/Sources/ComputeCxx/Debug/DebugServer.cpp b/Sources/ComputeCxx/Debug/DebugServer.cpp new file mode 100644 index 0000000..352be4e --- /dev/null +++ b/Sources/ComputeCxx/Debug/DebugServer.cpp @@ -0,0 +1,212 @@ +#include "DebugServer.h" + +#include +#include +#include +#include +#include +#include + +#include "Log/Log.h" + +namespace IAG { + +constexpr int backlog = 5; + +DebugServer *DebugServer::_shared_server = nullptr; + +DebugServer *_Nullable DebugServer::start(IAGDebugServerOptions options) { + if (options & IAGDebugServerOptionsEnabled && !_shared_server) { + if (true /* && os_variant_has_internal_diagnostics() */) { + _shared_server = new DebugServer(options); + } + } + return _shared_server; +} + +void DebugServer::stop() { + if (!_shared_server) { + return; + } + delete _shared_server; + _shared_server = nullptr; +} + +DebugServer::DebugServer(IAGDebugServerOptions options) : _socket(-1), _ip(0), _port(0), _token(arc4random()) { + + _socket = socket(AF_INET, SOCK_STREAM, 0); + if (_socket < 0) { + perror("IAGDebugServer: socket"); + return; + } + + fcntl(_socket, F_SETFD, FD_CLOEXEC); + + int option_value = 1; + setsockopt(_socket, SOL_SOCKET, SO_NOSIGPIPE, &option_value, sizeof(option_value)); + + sockaddr_in address = {}; + address.sin_family = AF_INET; + address.sin_port = 0; // Let system assign port + address.sin_addr.s_addr = (options & IAGDebugServerOptionsNetworkInterface) ? INADDR_ANY : htonl(INADDR_LOOPBACK); + + if (bind(_socket, (struct sockaddr *)&address, sizeof(address)) < 0) { + perror("IAGDebugServer: bind"); + shutdown(); + return; + } + + socklen_t length = sizeof(address); + if (getsockname(_socket, (struct sockaddr *)&address, &length) < 0) { + perror("IAGDebugServer: getsockname"); + shutdown(); + return; + } + + _ip = ntohl(address.sin_addr.s_addr); + _port = ntohs(address.sin_port); + + if (options & IAGDebugServerOptionsNetworkInterface) { + struct ifaddrs *ifaddr = nullptr; + if (!getifaddrs(&ifaddr)) { + for (auto *ifa = ifaddr; ifa != nullptr; ifa = ifa->ifa_next) { + if (ifa->ifa_addr && ifa->ifa_addr->sa_family == AF_INET) { + struct sockaddr_in *sa = (struct sockaddr_in *)ifa->ifa_addr; + uint32_t ip_data = ntohl(sa->sin_addr.s_addr); + if (ip_data != INADDR_LOOPBACK) { + _ip = ip_data; + break; + } + } + } + freeifaddrs(ifaddr); + } + } + + if (listen(_socket, backlog) < 0) { + perror("IAGDebugServer: listen"); + shutdown(); + return; + } + + dispatch_source_t accept_source = + dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, _socket, 0, dispatch_get_main_queue()); + dispatch_set_context(accept_source, this); + dispatch_source_set_event_handler_f(accept_source, accept_handler); + dispatch_resume(accept_source); + _accept_source = util::adopt_objc(accept_source); + + char ip_str[INET_ADDRSTRLEN]; + uint32_t ip_network = htonl(_ip); + inet_ntop(AF_INET, &ip_network, ip_str, sizeof(ip_str)); + + platform_log_info(misc_log(), "debug server graph://%s:%d/?token=%u", ip_str, _port, _token); + fprintf(stdout, "debug server graph://%s:%d/?token=%u\n", ip_str, _port, _token); +} + +DebugServer::~DebugServer() { shutdown(); } + +void DebugServer::accept_handler(void *context) { + DebugServer *server = reinterpret_cast(context); + + struct sockaddr address = {}; + socklen_t length = sizeof(address); + int connection_socket = accept(server->_socket, &address, &length); + if (connection_socket < 0) { + perror("IAGDebugServer: accept"); + return; + } + + fcntl(connection_socket, F_SETFD, FD_CLOEXEC); + + server->_connections.push_back(std::make_unique(server, connection_socket)); +} + +void DebugServer::close_connection(Connection *connection) { + auto iter = std::remove_if(_connections.begin(), _connections.end(), + [&connection](auto &candidate) -> bool { return candidate.get() == connection; }); + _connections.erase(iter, _connections.end()); +} + +void DebugServer::run(uint32_t timeout) { + fd_set writefds; + struct timeval tv; + + bool accepted = false; + while (!accepted || !_connections.empty()) { + FD_ZERO(&writefds); + FD_SET(_socket, &writefds); + + int nfds = _socket; + for (auto &connection : _connections) { + FD_SET(connection->socket(), &writefds); + if (connection->socket() > nfds) { + nfds = connection->socket(); + } + } + + tv.tv_sec = timeout; + tv.tv_usec = 0; + + int num_sockets_ready = select(nfds + 1, nullptr, &writefds, nullptr, &tv); + if (num_sockets_ready <= 0) { + if (errno == EAGAIN) { + continue; + } + perror("IAGDebugServer: select"); + return; + } + + // Check if server is ready + if (FD_ISSET(_socket, &writefds)) { + accept_handler(this); + accepted = true; + } + + // Process ready connections + uint64_t i = 0; + while (i < _connections.size()) { + Connection *connection = _connections[i].get(); + if (FD_ISSET(connection->socket(), &writefds)) { + FD_CLR(connection->socket(), &writefds); + Connection::handler(connection); + + // Restart loop to handle possible mutations to connections + i = 0; + } else { + ++i; + } + } + } +} + +void DebugServer::shutdown() { + if (auto accept_source = _accept_source.get()) { + dispatch_source_set_event_handler(accept_source, nullptr); + dispatch_set_context(accept_source, nullptr); + _accept_source = nullptr; + } + if (_socket >= 0) { + close(_socket); + _socket = -1; + } +} + +CFURLRef DebugServer::copy_url() { + if (_socket < 0) { + return nullptr; + } + + char ip_str[INET_ADDRSTRLEN]; + uint32_t ip_network = htonl(_ip); + inet_ntop(AF_INET, &ip_network, ip_str, sizeof(ip_str)); + + char bytes[0x100]; + snprintf_l(bytes, 0x100, nullptr, "graph://%s:%d/?token=%u", ip_str, _port, _token); + + CFIndex length = strlen(bytes); + return CFURLCreateWithBytes(kCFAllocatorDefault, (const unsigned char *)bytes, length, kCFStringEncodingUTF8, + nullptr); +} + +} // namespace IAG diff --git a/Sources/ComputeCxx/Debug/DebugServer.h b/Sources/ComputeCxx/Debug/DebugServer.h new file mode 100644 index 0000000..001148f --- /dev/null +++ b/Sources/ComputeCxx/Debug/DebugServer.h @@ -0,0 +1,81 @@ +#pragma once + +#include "ComputeCxx/IAGBase.h" + +#if TARGET_OS_MAC + +#include +#include +#include + +#include + +#include "ComputeCxx/IAGDebugServer.h" +#include "Vector/Vector.h" + +IAG_ASSUME_NONNULL_BEGIN + +namespace IAG { + +class DebugServer { + public: + class Connection { + private: + DebugServer *_server; + int _socket; + util::objc_ptr _event_source = nullptr; + + public: + Connection(DebugServer *server, int socket); + ~Connection(); + + int socket() const { return _socket; }; + + static void handler(void *context); + + friend class DebugServer; + }; + + private: + int _socket; + uint32_t _ip; + uint16_t _port; + uint32_t _token; + util::objc_ptr _accept_source = nullptr; + vector, 0, uint64_t> _connections; + + static DebugServer *_Nullable _shared_server; + + static void accept_handler(void *context); + void close_connection(Connection *connection); + + static CFDataRef _Nullable receive(Connection *connection, IAGDebugServerMessageHeader *header, CFDataRef body); + + public: + static DebugServer *shared() { return _shared_server; }; + + static DebugServer *_Nullable start(IAGDebugServerOptions options); + static void stop(); + + DebugServer(IAGDebugServerOptions options); + ~DebugServer(); + + // Non-copyable + DebugServer(const DebugServer &) = delete; + void operator=(const DebugServer &) = delete; + + // Non-movable + DebugServer(DebugServer &&) = delete; + DebugServer &operator=(DebugServer &&) = delete; + + void run(uint32_t timeout); + void shutdown(); + + CFURLRef _Nullable copy_url(); +}; + +} // namespace IAG + +IAG_ASSUME_NONNULL_END + +#endif diff --git a/Sources/ComputeCxx/Debug/DebugServer.mm b/Sources/ComputeCxx/Debug/DebugServer.mm new file mode 100644 index 0000000..005ea01 --- /dev/null +++ b/Sources/ComputeCxx/Debug/DebugServer.mm @@ -0,0 +1,82 @@ +#include "DebugServer.h" + +#import + +#include + +#include "ComputeCxx/IAGDescription.h" +#include "Graph/Graph.h" + +namespace IAG { + +CFDataRef DebugServer::receive(Connection *connection, IAGDebugServerMessageHeader *header, CFDataRef body) { + @autoreleasepool { + id body_json = [NSJSONSerialization JSONObjectWithData:(__bridge NSData *)body options:0 error:nullptr]; + if (!body_json) { + return nullptr; + } + if (![body_json isKindOfClass:[NSDictionary class]]) { + return nullptr; + } + + NSDictionary *body_dict = (NSDictionary *)body_json; + NSString *command = body_dict[@"command"]; + + if ([command isEqual:@"graph/description"]) { + NSMutableDictionary *options = [NSMutableDictionary dictionaryWithDictionary:body_dict]; + options[IAGDescriptionFormat] = @"graph/dict"; + + id desc = Graph::description(nullptr, options); + if (!desc) { + return nullptr; + } + + return (CFDataRef)CFBridgingRetain([NSJSONSerialization dataWithJSONObject:desc options:0 error:nil]); + } else if ([command isEqual:@"profiler/start"]) { + IAGGraphProfileFlags flags = IAGGraphProfileFlagsEnabled; + id flags_json = body_dict[@"flags"]; + if ([flags_json isKindOfClass:[NSNumber class]]) { + flags |= [flags_json unsignedIntValue]; + } + Graph::all_start_profiling(flags); + } else if ([command isEqual:@"profiler/stop"]) { + Graph::all_stop_profiling(); + } else if ([command isEqual:@"profiler/reset"]) { + Graph::all_reset_profile(); + } else if ([command isEqual:@"profiler/mark"]) { + id name_json = body_dict[@"name"]; + if ([name_json isKindOfClass:[NSString class]]) { + Graph::all_mark_profile([name_json UTF8String]); + } + } else if ([command isEqual:@"tracing/start"]) { + IAGGraphTraceFlags flags = IAGGraphTraceFlagsEnabled; + id flags_json = body_dict[@"flags"]; + if ([flags_json isKindOfClass:[NSNumber class]]) { + flags |= [flags_json unsignedIntValue]; + } + + auto subsystems = std::span(); + auto subsystems_vector = vector, 0, uint64_t>(); + id subsystems_json = body_dict[@"subsystems"]; + if ([subsystems_json isKindOfClass:[NSArray class]]) { + for (id subsystem_json in subsystems_json) { + if ([subsystem_json isKindOfClass:[NSString class]]) { + const char *str = strdup([subsystem_json UTF8String]); + subsystems_vector.push_back(std::unique_ptr(str)); + } + } + subsystems = std::span((const char **)subsystems_vector.data(), subsystems_vector.size()); + } + + Graph::all_start_tracing(flags, subsystems); + } else if ([command isEqual:@"tracing/stop"]) { + Graph::all_stop_tracing(); + } else if ([command isEqual:@"tracing/sync"]) { + Graph::all_sync_tracing(); + } + + return nullptr; + } +} + +} // namespace IAG diff --git a/Sources/ComputeCxx/Debug/IAGDebugServer.cpp b/Sources/ComputeCxx/Debug/IAGDebugServer.cpp new file mode 100644 index 0000000..f08806c --- /dev/null +++ b/Sources/ComputeCxx/Debug/IAGDebugServer.cpp @@ -0,0 +1,25 @@ +#include "ComputeCxx/IAGDebugServer.h" + +#include "DebugServer.h" + +void IAGDebugServerStart(IAGDebugServerOptions options) { IAG::DebugServer::start(options); } + +void IAGDebugServerStop(void) { IAG::DebugServer::stop(); } + +void IAGDebugServerRun(uint32_t timeout) { + auto debug_server = IAG::DebugServer::shared(); + if (!debug_server) { + return; + } + + debug_server->run(timeout); +} + +CFURLRef IAGDebugServerCopyURL(void) { + auto debug_server = IAG::DebugServer::shared(); + if (!debug_server) { + return nullptr; + } + + return debug_server->copy_url(); +} diff --git a/Sources/ComputeCxx/Graph/Graph.cpp b/Sources/ComputeCxx/Graph/Graph.cpp index 66a3d30..0d0dd0d 100644 --- a/Sources/ComputeCxx/Graph/Graph.cpp +++ b/Sources/ComputeCxx/Graph/Graph.cpp @@ -24,6 +24,7 @@ #include "ComputeCxx/IAGGraphTracing.h" #include "ComputeCxx/IAGUniqueID.h" #include "Context.h" +#include "Debug/DebugServer.h" #include "Graph/IAGAppObserver.h" #include "Graph/ProfileTrace.h" #include "KeyTable.h" @@ -54,6 +55,12 @@ Graph::Graph() static auto [profile_flags, trace_flags, trace_subsystems] = []() -> std::tuple, 0, uint64_t>> { + const char *debug_server = getenv("IAG_DEBUG_SERVER"); + if (debug_server) { + uint32_t options = (uint32_t)strtol(debug_server, nullptr, 0); + DebugServer::start(options); + } + uint32_t profile_flags = false; const char *profile_string = getenv("IAG_PROFILE"); if (profile_string) { diff --git a/Sources/ComputeCxx/include/ComputeCxx/ComputeCxx.h b/Sources/ComputeCxx/include/ComputeCxx/ComputeCxx.h index 0080144..4bbc5cd 100644 --- a/Sources/ComputeCxx/include/ComputeCxx/ComputeCxx.h +++ b/Sources/ComputeCxx/include/ComputeCxx/ComputeCxx.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include diff --git a/Sources/ComputeCxx/include/ComputeCxx/IAGDebugServer.h b/Sources/ComputeCxx/include/ComputeCxx/IAGDebugServer.h new file mode 100644 index 0000000..eefd292 --- /dev/null +++ b/Sources/ComputeCxx/include/ComputeCxx/IAGDebugServer.h @@ -0,0 +1,49 @@ +#pragma once + +#include + +#if TARGET_OS_MAC + +#include + +typedef void *IAGDebugServerRef IAG_SWIFT_STRUCT IAG_SWIFT_NAME(DebugServer); + +typedef IAG_OPTIONS(uint32_t, IAGDebugServerOptions){ + IAGDebugServerOptionsEnabled = 1 << 0, + IAGDebugServerOptionsNetworkInterface = 1 << 1, +} IAG_SWIFT_NAME(DebugServer.Options); + +typedef struct IAG_SWIFT_NAME(DebugServer.MessageHeader) IAGDebugServerMessageHeader { + uint32_t token; + uint32_t reserved1; + uint32_t body_length; + uint32_t reserved2; +} IAGDebugServerMessageHeader; + +IAG_ASSUME_NONNULL_BEGIN +IAG_IMPLICIT_BRIDGING_ENABLED + +IAG_EXTERN_C_BEGIN + +IAG_EXPORT +IAG_REFINED_FOR_SWIFT +void IAGDebugServerStart(IAGDebugServerOptions options) IAG_SWIFT_NAME(DebugServer.start(options:)); + +IAG_EXPORT +IAG_REFINED_FOR_SWIFT +void IAGDebugServerStop(void) IAG_SWIFT_NAME(DebugServer.stop()); + +IAG_EXPORT +IAG_REFINED_FOR_SWIFT +void IAGDebugServerRun(uint32_t timeout) IAG_SWIFT_NAME(DebugServer.run(timeout:)); + +IAG_EXPORT +IAG_REFINED_FOR_SWIFT +CFURLRef _Nullable IAGDebugServerCopyURL(void) IAG_SWIFT_NAME(getter:DebugServer.url()); + +IAG_EXTERN_C_END + +IAG_IMPLICIT_BRIDGING_DISABLED +IAG_ASSUME_NONNULL_END + +#endif From 7126555813fd3dc9373f955ddc30ce9e4cecabcc Mon Sep 17 00:00:00 2001 From: James Moschou Date: Fri, 14 Aug 2026 18:02:21 +0200 Subject: [PATCH 4/5] Add DebugServerTests --- .../Headers/AGDebugServer.h | 49 +++++++++++++ .../Headers/AttributeGraph.h | 1 + .../Versions/A/Headers/AGDebugServer.h | 49 +++++++++++++ .../Versions/A/Headers/AttributeGraph.h | 1 + .../Shared/Debug/DebugServerTests.swift | 47 +++++++++++++ .../Shared/TestSupport/DebugClient.swift | 70 +++++++++++++++++++ 6 files changed, 217 insertions(+) create mode 100644 CompatibilityTesting/Frameworks/AttributeGraph.xcframework/maccatalyst-arm64_arm64e_x86_64/AttributeGraph.framework/Headers/AGDebugServer.h create mode 100644 CompatibilityTesting/Frameworks/AttributeGraph.xcframework/macos-arm64_arm64e_x86_64/AttributeGraph.framework/Versions/A/Headers/AGDebugServer.h create mode 100644 Tests/ComputeTests/Shared/Debug/DebugServerTests.swift create mode 100644 Tests/ComputeTests/Shared/TestSupport/DebugClient.swift diff --git a/CompatibilityTesting/Frameworks/AttributeGraph.xcframework/maccatalyst-arm64_arm64e_x86_64/AttributeGraph.framework/Headers/AGDebugServer.h b/CompatibilityTesting/Frameworks/AttributeGraph.xcframework/maccatalyst-arm64_arm64e_x86_64/AttributeGraph.framework/Headers/AGDebugServer.h new file mode 100644 index 0000000..d7c3ff4 --- /dev/null +++ b/CompatibilityTesting/Frameworks/AttributeGraph.xcframework/maccatalyst-arm64_arm64e_x86_64/AttributeGraph.framework/Headers/AGDebugServer.h @@ -0,0 +1,49 @@ +#pragma once + +#include + +#if TARGET_OS_MAC + +#include + +typedef void *AGDebugServerRef AG_SWIFT_STRUCT AG_SWIFT_NAME(DebugServer); + +typedef AG_OPTIONS(uint32_t, AGDebugServerOptions){ + AGDebugServerOptionsEnabled = 1 << 0, + AGDebugServerOptionsNetworkInterface = 1 << 1, +} AG_SWIFT_NAME(DebugServer.Options); + +typedef struct AG_SWIFT_NAME(DebugServer.MessageHeader) AGDebugServerMessageHeader { + uint32_t token; + uint32_t reserved1; + uint32_t body_length; + uint32_t reserved2; +} AGDebugServerMessageHeader; + +AG_ASSUME_NONNULL_BEGIN +AG_IMPLICIT_BRIDGING_ENABLED + +AG_EXTERN_C_BEGIN + +AG_EXPORT +AG_REFINED_FOR_SWIFT +void AGDebugServerStart(AGDebugServerOptions options) AG_SWIFT_NAME(DebugServer.start(options:)); + +AG_EXPORT +AG_REFINED_FOR_SWIFT +void AGDebugServerStop(void) AG_SWIFT_NAME(DebugServer.stop()); + +AG_EXPORT +AG_REFINED_FOR_SWIFT +void AGDebugServerRun(uint32_t timeout) AG_SWIFT_NAME(DebugServer.run(timeout:)); + +AG_EXPORT +AG_REFINED_FOR_SWIFT +CFURLRef _Nullable AGDebugServerCopyURL(void) AG_SWIFT_NAME(getter:DebugServer.url()); + +AG_EXTERN_C_END + +AG_IMPLICIT_BRIDGING_DISABLED +AG_ASSUME_NONNULL_END + +#endif diff --git a/CompatibilityTesting/Frameworks/AttributeGraph.xcframework/maccatalyst-arm64_arm64e_x86_64/AttributeGraph.framework/Headers/AttributeGraph.h b/CompatibilityTesting/Frameworks/AttributeGraph.xcframework/maccatalyst-arm64_arm64e_x86_64/AttributeGraph.framework/Headers/AttributeGraph.h index bef19a0..b53cfd7 100644 --- a/CompatibilityTesting/Frameworks/AttributeGraph.xcframework/maccatalyst-arm64_arm64e_x86_64/AttributeGraph.framework/Headers/AttributeGraph.h +++ b/CompatibilityTesting/Frameworks/AttributeGraph.xcframework/maccatalyst-arm64_arm64e_x86_64/AttributeGraph.framework/Headers/AttributeGraph.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include diff --git a/CompatibilityTesting/Frameworks/AttributeGraph.xcframework/macos-arm64_arm64e_x86_64/AttributeGraph.framework/Versions/A/Headers/AGDebugServer.h b/CompatibilityTesting/Frameworks/AttributeGraph.xcframework/macos-arm64_arm64e_x86_64/AttributeGraph.framework/Versions/A/Headers/AGDebugServer.h new file mode 100644 index 0000000..d7c3ff4 --- /dev/null +++ b/CompatibilityTesting/Frameworks/AttributeGraph.xcframework/macos-arm64_arm64e_x86_64/AttributeGraph.framework/Versions/A/Headers/AGDebugServer.h @@ -0,0 +1,49 @@ +#pragma once + +#include + +#if TARGET_OS_MAC + +#include + +typedef void *AGDebugServerRef AG_SWIFT_STRUCT AG_SWIFT_NAME(DebugServer); + +typedef AG_OPTIONS(uint32_t, AGDebugServerOptions){ + AGDebugServerOptionsEnabled = 1 << 0, + AGDebugServerOptionsNetworkInterface = 1 << 1, +} AG_SWIFT_NAME(DebugServer.Options); + +typedef struct AG_SWIFT_NAME(DebugServer.MessageHeader) AGDebugServerMessageHeader { + uint32_t token; + uint32_t reserved1; + uint32_t body_length; + uint32_t reserved2; +} AGDebugServerMessageHeader; + +AG_ASSUME_NONNULL_BEGIN +AG_IMPLICIT_BRIDGING_ENABLED + +AG_EXTERN_C_BEGIN + +AG_EXPORT +AG_REFINED_FOR_SWIFT +void AGDebugServerStart(AGDebugServerOptions options) AG_SWIFT_NAME(DebugServer.start(options:)); + +AG_EXPORT +AG_REFINED_FOR_SWIFT +void AGDebugServerStop(void) AG_SWIFT_NAME(DebugServer.stop()); + +AG_EXPORT +AG_REFINED_FOR_SWIFT +void AGDebugServerRun(uint32_t timeout) AG_SWIFT_NAME(DebugServer.run(timeout:)); + +AG_EXPORT +AG_REFINED_FOR_SWIFT +CFURLRef _Nullable AGDebugServerCopyURL(void) AG_SWIFT_NAME(getter:DebugServer.url()); + +AG_EXTERN_C_END + +AG_IMPLICIT_BRIDGING_DISABLED +AG_ASSUME_NONNULL_END + +#endif diff --git a/CompatibilityTesting/Frameworks/AttributeGraph.xcframework/macos-arm64_arm64e_x86_64/AttributeGraph.framework/Versions/A/Headers/AttributeGraph.h b/CompatibilityTesting/Frameworks/AttributeGraph.xcframework/macos-arm64_arm64e_x86_64/AttributeGraph.framework/Versions/A/Headers/AttributeGraph.h index bef19a0..b53cfd7 100644 --- a/CompatibilityTesting/Frameworks/AttributeGraph.xcframework/macos-arm64_arm64e_x86_64/AttributeGraph.framework/Versions/A/Headers/AttributeGraph.h +++ b/CompatibilityTesting/Frameworks/AttributeGraph.xcframework/macos-arm64_arm64e_x86_64/AttributeGraph.framework/Versions/A/Headers/AttributeGraph.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include diff --git a/Tests/ComputeTests/Shared/Debug/DebugServerTests.swift b/Tests/ComputeTests/Shared/Debug/DebugServerTests.swift new file mode 100644 index 0000000..8a8b23a --- /dev/null +++ b/Tests/ComputeTests/Shared/Debug/DebugServerTests.swift @@ -0,0 +1,47 @@ +import Foundation +import Testing + +#if COMPATIBILITY_TESTS +// os_variant_has_internal_diagnostics("com.apple.AttributeGraph") returns false +let hasInternalDiagnostics = false +#else +let hasInternalDiagnostics = true +#endif + +@Suite(.enabled(if: hasInternalDiagnostics), .serialized(for: \DebugServer.self)) +struct DebugServerTests { + @Test + func urlOnlyAvailableWhileServerEnabled() throws { + DebugServer.start(options: [.enabled]) + + let url = try #require(DebugServer.url) + #expect(try /graph:\/\/127\.0\.0\.1:\d+\/\?token=\d+/.wholeMatch(in: "\(url)") != nil) + + DebugServer.stop() + #expect(DebugServer.url == nil) + } + + @Test + @available(macOS 26, iOS 26, tvOS 26, watchOS 26, *) + func debugServer() async throws { + DebugServer.start(options: [DebugServer.Options.enabled]) + defer { + DebugServer.stop() + } + + let url = try #require(DebugServer.url) + let client = try #require(DebugClient(url: url as URL)) + + let response = try await client.performCommand("graph/description") as? NSDictionary + #expect( + response == [ + "version": 2, + "counters": [ + "bytes": 0, + "max_bytes": 0, + ], + "graphs": [], + ] + ) + } +} diff --git a/Tests/ComputeTests/Shared/TestSupport/DebugClient.swift b/Tests/ComputeTests/Shared/TestSupport/DebugClient.swift new file mode 100644 index 0000000..c37feba --- /dev/null +++ b/Tests/ComputeTests/Shared/TestSupport/DebugClient.swift @@ -0,0 +1,70 @@ +import Foundation +import Network + +@available(macOS 26, iOS 26, tvOS 26, watchOS 26, *) +struct DebugClient { + private let endpoint: NWEndpoint + private let token: UInt32 + + init?(url: URL) { + guard + let components = URLComponents(url: url, resolvingAgainstBaseURL: false), + let host = components.host, + let port = components.port, + let queryItems = components.queryItems, + let tokenItem = queryItems.first(where: { $0.name == "token" }), + let tokenValue = tokenItem.value, + let token = UInt32(tokenValue), + let nwPort = NWEndpoint.Port(rawValue: UInt16(port)) + else { + return nil + } + self.endpoint = .hostPort(host: NWEndpoint.Host(host), port: nwPort) + self.token = token + } + + func performCommand(_ command: String, data: [String: Any] = [:]) async throws -> Any? { + var requestBody: [String: Any] = [ + "command": command + ] + for (key, value) in data { + requestBody[key] = value + } + + let requestBodyData = try JSONSerialization.data(withJSONObject: requestBody) + let requestHeader = DebugServer.MessageHeader( + token: self.token, + reserved1: 0, + body_length: UInt32(requestBodyData.count), + reserved2: 0 + ) + let requestHeaderData = withUnsafePointer(to: requestHeader) { pointer in + Data(bytes: UnsafeRawPointer(pointer), count: DebugServer.MessageHeader.size) + } + + var response: Any? + try await withNetworkConnection(to: endpoint, using: { TCP() }) { connection in + try await connection.send(requestHeaderData) + try await connection.send(requestBodyData) + + let responseHeaderMessage = try await connection.receive(exactly: DebugServer.MessageHeader.size) + let responseHeader = responseHeaderMessage.content.withUnsafeBytes { pointer in + pointer.baseAddress!.assumingMemoryBound(to: DebugServer.MessageHeader.self).pointee + } + precondition(responseHeader.token == token, "token mismatch") + + guard responseHeader.body_length > 0 else { + return + } + let responseBodyMessage = try await connection.receive(exactly: Int(responseHeader.body_length)) + response = try JSONSerialization.jsonObject(with: responseBodyMessage.content) + } + return response + } +} + +extension DebugServer.MessageHeader { + fileprivate static var size: Int { + MemoryLayout.size + } +} From cbc37f27045781aa66182d38e15e0c36b23d472e Mon Sep 17 00:00:00 2001 From: James Moschou Date: Fri, 14 Aug 2026 18:26:15 +0200 Subject: [PATCH 5/5] Don't include DebugServer on Linux --- Sources/ComputeCxx/Debug/Connection.cpp | 4 ++++ Sources/ComputeCxx/Debug/DebugServer.cpp | 4 ++++ Sources/ComputeCxx/Debug/DebugServer.mm | 4 ++++ Sources/ComputeCxx/Debug/IAGDebugServer.cpp | 4 ++++ Sources/ComputeCxx/Graph/Graph.cpp | 2 ++ Sources/Utilities/include/Utilities/ObjCPointer.h | 4 ++++ Tests/ComputeTests/Shared/Debug/DebugServerTests.swift | 3 +++ Tests/ComputeTests/Shared/TestSupport/DebugClient.swift | 3 +++ 8 files changed, 28 insertions(+) diff --git a/Sources/ComputeCxx/Debug/Connection.cpp b/Sources/ComputeCxx/Debug/Connection.cpp index 5bcc729..7ccb436 100644 --- a/Sources/ComputeCxx/Debug/Connection.cpp +++ b/Sources/ComputeCxx/Debug/Connection.cpp @@ -1,5 +1,7 @@ #include "DebugServer.h" +#if TARGET_OS_MAC + #include #include @@ -143,3 +145,5 @@ void DebugServer::Connection::handler(void *context) { } } // namespace IAG + +#endif diff --git a/Sources/ComputeCxx/Debug/DebugServer.cpp b/Sources/ComputeCxx/Debug/DebugServer.cpp index 352be4e..65ce750 100644 --- a/Sources/ComputeCxx/Debug/DebugServer.cpp +++ b/Sources/ComputeCxx/Debug/DebugServer.cpp @@ -1,5 +1,7 @@ #include "DebugServer.h" +#if TARGET_OS_MAC + #include #include #include @@ -210,3 +212,5 @@ CFURLRef DebugServer::copy_url() { } } // namespace IAG + +#endif diff --git a/Sources/ComputeCxx/Debug/DebugServer.mm b/Sources/ComputeCxx/Debug/DebugServer.mm index 005ea01..5033708 100644 --- a/Sources/ComputeCxx/Debug/DebugServer.mm +++ b/Sources/ComputeCxx/Debug/DebugServer.mm @@ -1,5 +1,7 @@ #include "DebugServer.h" +#if TARGET_OS_MAC + #import #include @@ -80,3 +82,5 @@ } } // namespace IAG + +#endif diff --git a/Sources/ComputeCxx/Debug/IAGDebugServer.cpp b/Sources/ComputeCxx/Debug/IAGDebugServer.cpp index f08806c..9d69d0b 100644 --- a/Sources/ComputeCxx/Debug/IAGDebugServer.cpp +++ b/Sources/ComputeCxx/Debug/IAGDebugServer.cpp @@ -1,5 +1,7 @@ #include "ComputeCxx/IAGDebugServer.h" +#if TARGET_OS_MAC + #include "DebugServer.h" void IAGDebugServerStart(IAGDebugServerOptions options) { IAG::DebugServer::start(options); } @@ -23,3 +25,5 @@ CFURLRef IAGDebugServerCopyURL(void) { return debug_server->copy_url(); } + +#endif diff --git a/Sources/ComputeCxx/Graph/Graph.cpp b/Sources/ComputeCxx/Graph/Graph.cpp index 0d0dd0d..d9c28ce 100644 --- a/Sources/ComputeCxx/Graph/Graph.cpp +++ b/Sources/ComputeCxx/Graph/Graph.cpp @@ -55,11 +55,13 @@ Graph::Graph() static auto [profile_flags, trace_flags, trace_subsystems] = []() -> std::tuple, 0, uint64_t>> { +#if TARGET_OS_MAC const char *debug_server = getenv("IAG_DEBUG_SERVER"); if (debug_server) { uint32_t options = (uint32_t)strtol(debug_server, nullptr, 0); DebugServer::start(options); } +#endif uint32_t profile_flags = false; const char *profile_string = getenv("IAG_PROFILE"); diff --git a/Sources/Utilities/include/Utilities/ObjCPointer.h b/Sources/Utilities/include/Utilities/ObjCPointer.h index 22c7e5c..b09ac1e 100644 --- a/Sources/Utilities/include/Utilities/ObjCPointer.h +++ b/Sources/Utilities/include/Utilities/ObjCPointer.h @@ -1,5 +1,7 @@ #pragma once +#if __has_include() + #include #include @@ -121,3 +123,5 @@ objc_ptr adopt_objc(T obj) noexcept { } // namespace util UTIL_ASSUME_NONNULL_END + +#endif diff --git a/Tests/ComputeTests/Shared/Debug/DebugServerTests.swift b/Tests/ComputeTests/Shared/Debug/DebugServerTests.swift index 8a8b23a..99cd644 100644 --- a/Tests/ComputeTests/Shared/Debug/DebugServerTests.swift +++ b/Tests/ComputeTests/Shared/Debug/DebugServerTests.swift @@ -1,3 +1,4 @@ +#if os(Darwin) import Foundation import Testing @@ -45,3 +46,5 @@ struct DebugServerTests { ) } } + +#endif diff --git a/Tests/ComputeTests/Shared/TestSupport/DebugClient.swift b/Tests/ComputeTests/Shared/TestSupport/DebugClient.swift index c37feba..1d4af0a 100644 --- a/Tests/ComputeTests/Shared/TestSupport/DebugClient.swift +++ b/Tests/ComputeTests/Shared/TestSupport/DebugClient.swift @@ -1,3 +1,4 @@ +#if os(Darwin) import Foundation import Network @@ -68,3 +69,5 @@ extension DebugServer.MessageHeader { MemoryLayout.size } } + +#endif