Reassembly was written primarily on OSX using Xcode for compiling/debugging and emacs for text editing. Xcode is clearly designed primarily for developing iOS “apps” and there are several non-obvious settings that vastly improve the desktop game development experience.
My general experience with Xcode has been fairly positive. I think Visual Studio is a slightly better debugger overall but not enough to make me abandon OSX. As a solo developer I need to spend a fair amount of time in cafes and other public places to avoid going insane and as far as I can tell Apple makes the best laptops.
My OSX builds are all 64 bit and have a deployment target of OSX 10.7. The following was written with reference to Xcode 6.1.1.
~/.lldbinit
This config file is read by lldb, the llvm debugger, which is the backend for the xcode debugger. Settings effect the xcode visual debugger window in addition to the console.
Print 64 bit unsigned values in hex and show a summary for vector types.
type format add -f hex uint64 "unsigned long long" type summary add --inline-children --omit-names float2 double2 cpVect int2 float3
Print a summary for std::pair types (particularly useful for browsing std::map and std::unordered_map).
type summary add --inline-children --omit-names -x "^std::pair<.+>(( )?&)?$"
Print the address, “name” and “blocks” fields of custom type BlockCluster.
type summary add --summary-string "{addr=${var%V} name=${var.name} ${var.blocks}}" BlockCluster
One warning – invalid summary strings often crash Xcode. Restarting the debugging sessions is enough to reload .lldbinit after changes.
Develop No-resource build
For rapid iteration with good performance, I created a “Develop NoResource” target. This build has optimizations enabled (-O3), but no linktime codegen (which takes a long time). While iterating on code remove the main data directory from the “Copy Bundle Resources” build phase to prevent it from being copied every time. As long as you don’t clean previously copied resources will remain in the app directory.
In general lldb is “OK” at debugging optimized builds. It frequently looses the “this” pointer and stack variables but is nonetheless usually useful enough to get the job done. This is one of the areas where Visual Studio really shines, particularly with the magic option /d2Zi+ – see this Random ASCII post. Unfortunately C++ Debug builds, which the debugger readily understands, are usually too slow to be useful.
I have not been able to get the lldb debugger to call inlined stl symbols (e.g. std::vector::size) from the command line in optimized builds. If anyone knows how to do this please tell me.
Platform layers
I haven’t been able to completely abstract OS details into a third party platform independent layer like SDL. Reassembly uses SDL for window and event handling on windows and linux but is pure Cocoa on OSX. The Cocoa programming model is different enough from SDL that some level of native-ness would have been lost, and the amount of code required is not prohibitively large.
In general the Cocoa/Mac programming model is vastly nicer than win32 or POSIX/Linux. It does not carry 30 years of baggage, never refers to 16 bit OSs in in the docs, and provides easy to use APIs for common operations. Using the native API does not take more than a day or two to setup and forever after allows exact control and the ability to debug platform-specific bugs.
Once exception is the OSX Gamepad APIs which are unforgivably convoluted. Reassembly uses SDL to handle gamepads on OSX.
Command Line Builds
Xcode allows building projects from the command line. This is frequently convenient from e.g. emacs or as part of a release script.
xcodebuild -scheme "Reassembly Develop NoResources" \ -workspace Reassembly.xcodeproj/project.xcworkspace \ -jobs 4 -parallelizeTargets ONLY_ACTIVE_ARCH=YES
Note that, unlike with visual studio, xcode command line builds do not build the same target as the IDE and so you can’t do a command line build then switch to the IDE for debugging without doing an IDE build. Set the command line config for the non-root project in Project Settings -> Info.
For better interoperability with non-Xcode tools you can set the output directory of the compile .app in Xcode settings (command-,) under the Locations tab (see Derived Data).
Nested Projects
A little-known feature of Xcode is the ability to add .xcodeproj files as subprojects to your main project file. This is analogous to adding various visual studio projects to the solution file. I generally find it easier to integrate 3rd party code as source vs. using static libs, dlls, dylibs, .so, etc. Creating or adding a separate .xcodeproj per library allows me to specify different compile time options for this code.
Crash Handling
OSX is a POSIX compatible OS which means we can use unix signals to handle Segmentation Faults and other joyous occurrences. Automatically handling game crashes and uploading stack traces to my webserver has been a powerful tool in increasing Reassembly stability, and probably a topic for another article in and of itself. Most of my OSX crash handling code was shamelessly copied from this article on Atomic Objects. Make sure to disable the signal handlers in debug/develop builds or the debugger won’t break on crashes.
The standard backtrace and backtrace_symbols calls work from signal handlers on OSX as expected. backtrace_symbols works even on release binaries and unlike Linux does not require the -rdynamic linker option.
Here is code for printing out some relevant registers on OSX or Linux after catching a SIGSEGV or similar:
const ucontext_t *ctx = (ucontext_t*)context; const mcontext_t &mcontext = ctx->uc_mcontext; #if __APPLE__ const uint ecode = mcontext->__es.__err; #else const greg_t ecode = mcontext.gregs[REG_ERR]; #endif string msg0 = str_format("Invalid %s to %p", (ecode&4) ? "Exec" : (ecode&2) ? "Write" : "Read", siginfo->si_addr); ReportPOSIX(msg0); message += msg0 + "\n"; string mmsg; #if __APPLE__ #ifdef __LP64__ mmsg = str_format("PC/RIP: %#llx SP/RSP: %#llx, FP/RBP: %#llx", mcontext->__ss.__rip, mcontext->__ss.__rsp, mcontext->__ss.__rbp); #else mmsg = str_format("PC/EIP: %#x SP/ESP: %#x, FP/EBP: %#x", mcontext->__ss.__eip, mcontext->__ss.__esp, mcontext->__ss.__ebp); #endif #else #ifdef __LP64__ mmsg = str_format("PC/RIP: %#llx SP/RSP: %#llx, FP/RBP: %#llx", mcontext.gregs[REG_RIP], mcontext.gregs[REG_RSP], mcontext.gregs[REG_RBP]); #else mmsg = str_format("PC/EIP: %#x SP/ESP: %#x, FP/EBP: %#x", mcontext.gregs[REG_EIP], mcontext.gregs[REG_ESP], mcontext.gregs[REG_EBP]); #endif #endif ReportPOSIX(mmsg);
Threading
By default c++11 std::threads created under OSX have some ridiculously tiny amount of stack space. I use pthreads directly to create threads to overcome this – std::mutex and friends work fine with pthread threads (std::thread wraps pthreads anyway under OSX).
You can also set the current thread name such that it will show up in the Xcode thread view via a non-standard pthread API.
Creating Pthread with increased stack size
#if __APPLE__ typedef pthread_t OL_Thread; #define THREAD_IS_SELF(B) (pthread_self() == (B)) #define THREAD_ALIVE(B) (B) OL_Thread thread_create(void *(*start_routine)(void *), void *arg) { int err = 0; pthread_attr_t attr; pthread_t thread; err = pthread_attr_init(&attr); if (err) ReportMessagef("pthread_attr_init error: %s", strerror(err)); err = pthread_attr_setstacksize(&attr, 8 * 1024 * 1024); if (err) ReportMessagef("pthread_attr_setstacksize error: %s", strerror(err)); err = pthread_create(&thread, &attr, start_routine, arg); if (err) ReportMessagef("pthread_create error: %s", strerror(err)); return thread; } void thread_join(OL_Thread &thread) { if (!thread) return; int status = pthread_join(thread, NULL); ASSERTF(status == 0, "pthread_join: %s", strerror(status)); } #else typedef std::thread OL_Thread; #define THREAD_IS_SELF(B) (std::this_thread::get_id() == (B).get_id()) #define THREAD_ALIVE(B) ((B).joinable()) OL_Thread thread_create(void *(*start_routine)(void *), void *arg) { return std::thread(start_routine, arg); } void thread_join(OL_Thread& thread) { if (!thread.joinable()) return; try { thread.join(); } catch (std::exception &e) { ASSERT_FAILED("std::thread::join()", "%s", e.what()); } } #endif
Naming threads
You can use the pthreads API to set the current thread name, and this name will show up in the Xcode debugger which is extremely helpful. Unfortunately the non-standard pthreads API for this is different across OSX and Linux (and of course windows) – code for all platforms follows, this works in GDB and Visual Studio.
#if _WIN32 // // Usage: SetThreadName (-1, "MainThread"); // const DWORD MS_VC_EXCEPTION = 0x406D1388; #pragma pack(push,8) typedef struct tagTHREADNAME_INFO { DWORD dwType; // Must be 0x1000. LPCSTR szName; // Pointer to name (in user addr space). DWORD dwThreadID; // Thread ID (-1=caller thread). DWORD dwFlags; // Reserved for future use, must be zero. } THREADNAME_INFO; #pragma pack(pop) void SetThreadName(DWORD dwThreadID, const char* threadName) { THREADNAME_INFO info; info.dwType = 0x1000; info.szName = threadName; info.dwThreadID = dwThreadID; info.dwFlags = 0; __try { RaiseException(MS_VC_EXCEPTION, 0, sizeof(info) / sizeof(ULONG_PTR), (ULONG_PTR*)&info); } __except (EXCEPTION_EXECUTE_HANDLER) { } } #endif void thread_setup(const char* name) { uint64 tid = 0; #if _WIN32 tid = GetCurrentThreadId(); SetThreadName(tid, name); #elif __APPLE__ pthread_threadid_np(pthread_self(), &tid); pthread_setname_np(name); #else // linux tid = pthread_self(); int status = 0; // 16 character maximum! if ((status = pthread_setname_np(pthread_self(), name))) ReportMessagef("pthread_setname_np(pthread_t, const char*) failed: %s", strerror(status)); #endif }
Github
All of the code above is part of Reassembly and is available in context on my Outlaws core github project – see the os/osx directory for mac specific code.