LiveChat for Mac & iOS behind the scenes

09 Feb 2012

The purpose of this article is to present several technical design concepts implemented and proven to work well for over one year, since the first release of the Mac version of LiveChat operator application.

Back in 2010 when I started implementing the Mac version, the main task was to reach the feature level of the Windows version (developed for several years) as fast as possible, and also to prepare ground for an upcoming iOS version (available now in App Store). At that time I neither had access to the Windows version’s source code, nor was going to reuse any of it. Therefore I have settled the following constraints that yielded the following results (described in detail in following sections):

Architecture

1. Source code must be divided into a framework part and GUI part

Where the framework part should contain most of the application logic and use API available on both Mac & iOS platforms, while the GUI part should be reduced to a minimum. Altogether it should match Model-View-Controller architecture.

  • Protocol.framework is now 60% of both platforms total source code, manages server communication and models such as LCVisitor or LCChat.

  • Protocol.framework does not use any platform specific API and relies only on Foundation.framework.

  • Mac & iOS use separate source code for views & controllers driving GUI, however these contain almost no application logic.

2. Source code should be self-describing and object-oriented

Utilizing (where possible) dynamic facilities of Objective-C.

  • All model classes visible to controllers are Objective-C classes, controllers have no direct access to protocol.

  • Application logic talks with the server through an OO proxy object using dynamic message dispatch to translate messages back and forth into text protocol commands. This makes the protocol layer really small in comparison to the whole application logic.

Protocol.framework

Protocol.framework contains most of the application logic, manages creating and releasing model objects of LCVisitor, LCOperator & LCChat classes, and supplies GUI controllers with them. It contains LCServer private class which is responsible for “message to protocol command mapping”. Applications’ controllers are delegates of LCAccount class.

Mac version may dynamically follow model object’s state updates via Cocoa bindings (not available for iOS) without a need to manually process delegate methods. Therefore, for example inspector pane uses only bindings to update its contents, without actually knowing that it is part of complicated program logic. Inspector view contains just a layout and names for model properties presented to the user.

Strengths:

  • Putting whole application logic into Protocol.framework makes platform dependent GUI code reduced to minimum and responsible only for seamless translation of logical objects into GUI views.

  • Protocol.framework relies only on Foundation.framework that is a OO bridge for CoreFoundation and CFNetwork C libraries.

These libraries were published open-source and are known to build on several other platforms than OSX & iOS. Therefore in theory this code is portable to other platforms as well. Unfortunately source code of CFNetwork is no longer updated. Last available version matches OSX 10.4.11 Tiger.

Moreover CoreFoundation on other platforms is stripped from some functionality (named as CFLite) and latest releases have serious build problem on non-OSX platforms. There are several efforts to bring OSX API to other platforms, i.e.: OpenCFLite & PureFoundation fixing and extending CF & CFNetwork sources, Cocotron redoing whole Cocoa API.

Weaknesses:

  • Application must present same logic on all platforms. It is hard to code any exceptions.

  • Reusing Platform.framework to other platforms is possible in theory but problematic in practice. If all LiveChat applications were to use single protocol framework, C++ would be a better choice.

Server OO Proxy

While LCAccount translates messages received into model objects, LCServer class is responsible for translating textual protocol commands to Objective-C messages sent and received by LCAccount. LCServer class implementation is really small in comparison to other classes.

Once LCAccount wants to initiate server communication using LCServer, it needs first to provide protocol command to selector maps: (1) incoming map mapping to LCAccount methods, and (2) outgoing map mapping to LCOutgoingMethods protocol. LCServer is the lowest level class that works directly with sockets. Without going into details LiveChat protocol resembles MSN and thanks to its linear (non-structural) construction it is possible to implement such mapping.

Command Reception

Given that following incoming map is provided:

static LCCommandMapping incomingMap[] = {
  // …
  { @"R0004", @selector(didReceiveFromClientWithIdentifier:message:senderNick:conferenceIdentifier:hidden:)},
  // …
  { nil }
};

Upon each command reception LCServer, parses NSMethodSignature of mapped method and marshals all incoming command arguments into method arguments doing NSString to argument type conversion.

It supports all basic types such as BOOL, NSInteger and NSString. It also supports NSArrays for more advanced callback based commands (beyond the scope of this article). Altogether, calls when receiving following message: R0004|12341|Hello!|Joe|2134|0<LF>

[account didReceiveFromClientWithIdentifier:12341
                                    message:@"Hello!"
                                 senderNick:@"Joe"
                       conferenceIdentifier:2134
                                     hidden:NO];

Where called method has the following signature:

- (void)didReceiveFromClientWithIdentifier:(NSInteger)clientIdentifier
                                   message:(NSString *)message
                                senderNick:(NSString *)senderNick
                      conferenceIdentifier:(NSInteger)conferenceIdentifier
                                    hidden:(BOOL)hidden

Of course this method is not called directly (like presented above), but in fact LCServer:

  1. Constructs proper NSInvocation object via [NSInvocation invocationWithMethodSignature:methodSignature]

  2. Does marshaling and type conversion of received string arguments via [methodSignature getArgumentTypeAtIndex:index] example for NSUInteger argument:

   if (!strcmp(type, "Q")) {
     NSUInteger value = [string integerValue];
     [invocation setArgument:&value atIndex:index + 2];
   }
  1. finally calls [invocation invoke].

LCServer also handles situations when there are too many or too few arguments received from the server:

  1. In case there are too few, we may be working with an older server and all missing method arguments get nil, numeric get 0.

  2. When there are too many arguments, then we are working with a server more recent than the application. In such case the application ignores extra arguments and emits a warning in the log.

Command Issue

Sending protocol commands to server is done in an opposite way. This time LCServer acts as OO proxy to LCAccount that provides methods mapped to protocol command and handled via Objective-C message forwarding mechanism implemented by:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
  struct objc_method_description mdesc =
    protocol_getMethodDescription(outgoingProtocol, selector, YES, YES);
  if (mdesc.types == NULL) {
    return nil;
  }
  return [NSMethodSignature signatureWithObjCTypes:mdesc.types];
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
  NSString *commandName =
    (NSString *)CFDictionaryGetValue(outgoingCommandMap,
                                     (const void *)invocation.selector);
  // … converts arguments to string representation and send command over socket
}

Once LCAccount provides LCServer an outgoing map and proxy interface:

static LCCommandMapping outgoingMap[] = {
  // …
  { @"S0004", @selector(sendFromIdentifier:toChatWithIdentifier:message:) },
  // …
  { nil }
};
@protocol LCOutgoingCommands
// …
- (void)sendFromIdentifier:(NSInteger)senderIdentifier
      toChatWithIdentifier:(NSInteger)chatIdentifier
                   message:(NSString *)message;
// …
@end

It can cast internally LCServer to id<LCOutgoingCommands> and send message to LCServer:

[server sendFromIdentifier:431
      toChatWithIdentifier:5488
                   message:@"Hello to you too!"];

Strengths:

  1. Simplicity: No need to maintain long switch & case statements.

  2. Separation: Only simple LCServer private class has direct access to the protocol.

  3. Argument type casting done automatically at method invocation level.

  4. Easy to debug: If program crashes we can deduce command sent by the server from stack backtrace.

Weaknesses:

  1. All protocol commands must follow the same rules for formatting and escaping arguments. It is really problematic to handle any exceptions in this model.

Protocol commands themselves must be linear (non-structural). Some recently added LiveChat protocol commands have structural arguments that must be parsed separately via external parsers, like XML parser for add-on action commands.

Conclusion

I believe it would not be possible to easily implement solutions described in this article with other API and languages such as C++ or Java. Unfortunately I observe that Objective-C dynamic facilities are less and less utilized by recent Mac OS X (Cocoa) and iOS (Mobile Cocoa) releases and newcomer developers. For example iOS (Mobile Cocoa), which is kind of a rewrite of OSX Cocoa does not implement bindings, like if they were considered deprecated.

Altogether sparse documentation for Objective-C principles, lack of solid examples (for bindings) and finally company politics that intentionally turn this powerful, dynamic language & its APIs into close environment supporting selected products only, deny original ideas of keeping NeXTStep and Objective-C solutions for modern applications running on all platforms.

Posted in Objective-C, Programming