This article describes concepts and features with regards to invocation based applications. If you have never worked with invocation based applications before you should check out the tutorial.
As an Objective-C developer you are familiar with two basic concepts of object-orientated, class based programming languages:
Objective-Cloud brings this concepts to the cloud. This means that you can send a message to an object located on our servers, whose construction plan is stored in a class.
In most cases invocation based services are the first choice. They are very similar to what you know from your daily development routine. Especially when your client is developed in Objective-C as well, you should use invocation based services.
The forthcoming client framework supports message sending to the invocation bases services. You do not have to care about the transport any more.
However there are reasons that are less optimal with invocation based services. You should not use invocation based services,
Take in account, that Objective-C messaging is neutral to the transport. Using HTTP features heavily simply means that you add a secret information. It is a better way to put that information on a parameter instead of HTTP.
There are several core concepts you should understand:
Objective-Cloud is created for Objective-C developers. That means, that when developing something for Objective-Cloud you do that by using Objective-C.
On the other hand services running on Objective-Cloud should be usable by everybody. That means, that the public interface of a service should not relay on anything that is specific to Objective-C. Objective-Cloud solves this by using two mature technologies: JSON for the content and HTTPS for the transport. The reality is that everybody is able to send a HTTP request which contains a JSON document. Relying only on JSON and HTTP has some benefits:
By now Objective-Cloud supports stateless services: An (remote) invocation causes a method to be executed that returns a value. No state is stored for the next message, no side effects exist. This makes it possible to parallelize the execution of your code.
Of course, you can have state by passing instance objects as arguments to a service and the service can deliver a new state using instance objects as return value or via changed arguments parameters. Another pattern is to store a state in a data base as supported by Objective-Cloud.
There is an object oriented programming paradigm which says that states are stored in objects. For Objective-C this applies to (stateful) instance objects, but not to class objects, which are stateless. Consequently the receiver of a message is a class object. This is possible, because class objects are first class objects in Objective-C and support polymorphism and late binding. In contrast to other programming languages there is no need to introduce an anti-conceptual "state-losing stateful objects" (like Servlets) to get the basic features of object oriented programming.
Objective-Cloud uses a strict client-server architecture: A message is sent from a client to the server to executed code on the server. This corresponds to method invocations on a local machine.
Because of this strict concept, arguments are passed by value (they are copied) within a message invocation. Methods invoked on arguments are always executed on the server’s copy. After executing a method, you can send the argument automatically back to the client as described [here].
Maybe you are familiar with distributed objects (DO) and XPC services (XPC), technologies already existing in Cocoa. But because of the nature of Objective-Cloud there are some differences:
id
typed arguments are still possible.An invocation based service is always provided by a class and its methods. Such a class is called a public class, invocable methods are called public methods (public in the context of Objective-Cloud always means "reachable from a client's point of view").
The implementation of a service consists of three parts:
All these components are standard Objective-C well-known to you.
The cloud interface is a protocol that contains the methods that should be reachable from clients. These are called the public methods.
1 // ServiceCloudInterface.h
2 #import <OCFoundation/OCFoundation.h> // imports <Foundation/Foundation.h>
3
4 @protocol ServiceCloudInterface <NSObject>
5 + (NSString*)concat:(NSString*)firstPart with:(NSString*)secondPart;
6 @end
By convention the name of a protocol acting as a cloud interface ends with CloudInterface
. The Objective-Cloud runtime environment relies on this convention and accepts only methods declared by protocols following this convention. The publicity of methods from the base class is automatically inherited. It is not necessary to include them in the cloud interface of the subclass.
If your a class implements a cloud interface specific to a class, it is best practice to name the cloud interface like the class and append `CloudInterface`. If a cloud interface is used by more than one class – like usual protocols in Objective-C – you can choose a different name, but the suffix still has to be `CloudInterface`.
// Cloud interface of a class 'Service'
@protocol ServiceCloudInterface <NSObject>
+ (NSString*)concat:(NSString*)firstPart with:(NSString*)secondPart;
@end
// Cloud interface not related to a specific class
@protocol XMLCloudInterface <NSObject>
+ (NSDictionary*)xmlRepresentation
@end
// A class using its own and a common cloud interface
@interface Service : OCFPublicObject <ServiceCloudInterface, XMLCloudInterface>
@end
The interface of a public class is formed as usual. There are only two things that distinguish an interface of a public class from a "normal" class: It inherits from OCFPublicObject
and it implements a cloud interface.
// Service.h
#import "ServiceCloudInterface.h"
@interface Service : OCFPublicObject <ServiceCloudInterface>
@end
You can add methods to the class interface that should be public inside the cloud application, but private to the client. A public class can have more than one cloud interface.
// Service.h
#import "ServiceCloudInterface.h"
@interface Service : OCFPublicObject <ServiceCloudInterface, ActivityCloudInterface>
@end
As you can see, the Service
is derived from OCFPublicObject
.
Change incoming: This will change in near future: Every class can export a cloud interface. So you should not take any advantage of being the public class a subclass of OCFPublicObject
. (I. e. expecting messages to super to be executed inside OCFPublicObject
or using `OCFPublicObject` as a stop class for iterations.)
// Service.m
#import "Service.h"
@implementation Service
+ (NSString*)concat:(NSString*)firstPart with:(NSString*)secondPart
{
return [firstPart stringByAppendingString:secondPart];
}
@end
+ (NSString*)methodThatCanFailWithError:(NSError**)error
{
…
if( somethingFailed )
{
*error = [NSError …]; // Pass this object back to the sender
return nil;
}
return …;
}
error
points to an object reference and *error
equals nil
. If you want to be such a parameter bidirectional, you have to mark it with inout
.
+ (NSString*)methodThatCanFailWithError:(inout NSError**)error
{
…
if( somethingFailed )
{
*error = [NSError …]; // Pass this object back to the sender
return nil;
}
return …;
}
+ (void)methodThatChanges:(NSMutableString*)arg
{
[arg appendString:@" Negm"];
}
This is a patter to avoid in a local application. The sender of a message cannot recognize, whether an argument is changed inside a method or not. It might be easy in this case, because there is no other reason to pass an instance of NSMutableString
instead of NSString
. But this only applies to a hand-full classes that build a mutable/immutable class pair. Anyway, it is supported with Objective-Cloud.
By default Objective-Cloud takes the argument from JSON, but does not send the argument back to the sender of the original message. Any change to the argument is made locally on the server. To send the changed object back, you have to mark that with inout
:
+ (void)methodThatChanges:(inout NSMutableString*)arg
{
[arg appendString:@" Negm"];
}
The usual and most convenient way to return a result of the processing is to use C's return
. As you can see in the samples, there is nothing unusual about it. You do not have to use completion blocks and other boiler plate code to make things work. (For asynchronous responses see Responding asynchronously.)
For every invocation an instance object of OCFPublicInvocation is created. You can access it with +currentInvocation
(OCFPublicInvocation
). There are some reasons for doing so:
If your code inside the method uses an asynchronous API, it is inconvenient to return a result in C style:
+ (NSString*)methodUsingAsyncAPI
{
__block NSString* returnValue;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0L);
[Helper doSomethingAsyncWithCompletionHandler:^(NSString* result)
{
returnValue = result;
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return returnValue;
}
Therefore you can tell the Objective-Cloud's runtime environment to ignore the (synchronous) return value and provide a return value asynchronously to make the code more readable. To do so, you read the invocation object and send the respond using this. This strategy is told to the runtime environment by returning an ignore value.
+ (NSString*)methodUsingAsyncAPI
{
OCFPublicInvocation *invocation = [self currentInvocation];
[Helper doSomethingAsyncWithCompletionHandler:^(NSString* result)
{
[invocation returnJSONWithReturnValue:result];
}];
return OCFResponsesUsingRequest;
}
It is important that you read the invocation object in the synchronous control flow of your method. If you try to retrieve it in the – potentially asynchronous – block, it may fail.
All public methods run asynchronously and do not block. So you do not have to dispatch a block asynchronously, if it is a long runner. We have already done this for you.
Sometimes you do not want to encapsulate the return value in JSON. You are free to return any data, if you want to. The invocation object has some methods for doing so:
+ (NSString*)sayHello
{
OCFPublicInvocation *invocation = [OCFPublicInvocation currentInvocation];
[invocation returnPlainTextWithString:@"hello\n"];
return OCFResponsesUsingRequest;
}
This method responds
hello\n
instead of
{
returnValue : "hello\n"
}
OCFPublicInvocation
has convenient methods for other types including HTML and binary data.
You have access to the underlying HTTP request via +request
(OCFPublicInvocation
). It returns the raw OCFRequest
object as you will get as a parameter in a handler block developing a request based service. This offers you the complete abilities of request based applications for receiving requests and sending responses.
+ (void)requestBasedMethod
{
OCFPublicInvocation *invocation = [OCFPublicInvocation currentInvocation];
OCFRequest *request = invocation.request;
// …
}
In Objective-C data exchange in a message is done with arguments and return values. The message and invocation mechanism is transparent. If you heavily use properties of the request and its response in invocation based services, you probably do something wrong. For example, in Objective-C data is stored using setters and is read using getters. Reading HTTP methods instead to make your invocation based service RESTish is mixing up two worlds into one. On the other hand a pattern is as good as its exceptions: There might be reasons to introspect the request or set the respond on the level of HTTP.
If your client application is developed in Objective-C, too, you should always use invocation based applications and never relay on the HTTP layer. With the client framework messages and invocation become transparent again.
The examples so far used instance objects of classes that are at least strongly related to JSON types. But Objective-Cloud offers you the full flavor of classes. This explicitly includes easy usage of classes that are related to JSON out of the box and custom classes defined by you.
As an Objective-C developer you are familiar with dynamic typing. Running code on a server, which is reachable for everybody, dynamic typing becomes a security problem: While wrong typing (like passing an instance of NSString
, where a parameter is typed NSNumber *
) inside your application leads to warnings that you can fix, even a bug free cloud application can receive values of wrong types. Therefore the type of every value has to be checked before it is used.
Because this will lead to a mass of boiler plate code, we automized checking in nearly all cases. But there are still two cases we need help:
id
every instance is of an allowed class. You have to check, whether it is an expected class.id
typed, but it becomes a different shade in this context.)Every value (instance object) and every type used in your source code has a base type. This is the type after removing all indirections. Some examples:
Type | Base Type |
---|---|
id |
id |
NSString* |
NSString |
NSError** |
NSError |
Class |
Class |
Since scalar (C-ish) types like int
, char
, … are not allowed, the base type is always a class including id
.
For some reasons it is helpful to distinguish between groups of types:
NSNumber
(JSON boolean and JSON number), NSString
, NSArray
, NSDictionary
(JSON object)NSMutableString
, NSMutableArray
, NSMutableDictionary
NSSet
, NSOrderedSet
, NSIndexSet
and the subclasses NSMutableSet
, NSMutableOrderedSets
, resp. NSMutableIndexSet
. NSData
, NSError
, and NSURL
provided by OCFoundation out of the boxOnly the last group of classes ("other classes") are not supported out of the box. You can add support for them very easily as described in the chapter "Custom parameter types".
The Objective-C runtime environment does not provide information about the exact type of an object reference parameter. All types are simply "object reference" (@
). We retrieve the additional information from source code. Doing this job we developed a custom compiler using clang's Lib Tooling called Cloud Interface Exporter. We call this technology "A type hardened Objective-C" (@hoc). Therefore the described type transformation and checking are only available, if the cloud interface protocol is compiled with the cloud interface exporter as done by default for the generated service template you clone from our servers. For classes added by you, you have to add them to the Export Cloud Interface build phase.
Every Objective-C class corresponds to a JSON type:
Cocoa Class | JSON Type | Example |
---|---|---|
NSNumber | boolean number |
true 1.23 |
NSString NSMutableString |
string | "Cloud" |
NSArray NSMutableArray |
array | [ 1, "Cloud", yes ] |
NSDictionary NSMutableDictionary |
object¹ | { "firstname" : "Amin", "lastname" : "Negm" } |
Other | object² | { "__com.objective-cloud.objectType" : "Person:NSObject", "firstname" : "Amin", "lastname" : "Negm" } |
¹The object must not contain the key "com.objective-cloud.objectType". ² The object has to contain the key "com.objective-cloud.objectType" with the class inheritance as its value as described here.
As mentioned before it is possible to have parameters of a custom type like Person
. Supporting a custom type boils down to make the type JSON-serializable. There are two basic approaches:
NSData
. In this document these classes are called containers.It is possible to encode an object using `NSCoding`. You can do this using one of the above methods or by typing your parameter to `NSData`. But there are two good reasons not to do so: 1. `NSCoding` describes the state of an object to be persisted, not the state of an object to be public. 2. `NSCoding` is proprietary to Cocoa.
The easiest way to convert an object to JSON and back is to define a cloud interface for the class of the object. You simply do this by enumerating the properties that should be transmitted:
@protocol PersonCloudInterface
@property NSString *name;
@property NSNumber *age;
@end
To link the protocol to the class, simply add it in its interface
@interface Person : NSObject <PersonCloudInterface>
@end
You can repeat the properties in the class interface to get automatic synthesization of accessors.
When the object is constructed from JSON, it receives messages in this order:
alloc
message.init
message.In some cases it is inconvenient to declare the object's content with properties. You can use NSCoding
-style JSON encoding for that. You have to implement to methods -encodeWithJSONEncoder:error:
and +newWithJSONDecoder:error:
.
- (BOOL)encodeWithJSONEncoder:(id<OCFJSONEncoder>)encoder error:(NSError **)error
{
[encoder encodeObject:… forKey:@"base64" error:error];
return YES;
}
+ (id)newWithJSONDecoder:(id<OCFJSONDecoder>)decoder error:(NSError **)error
{
NSString *base64 = [decoder decodeObjectOfClass:[NSString class] forKey:@"base64" error:error];
if (base64==nil) {
return nil;
}
NSData *data = [[NSData alloc] initWithBase64EncodedString:base64 options:0];
return data;
}
How to use values of custom classes is described here.
After developing a invocation base service you can invoke it's methods by using HTTP and JSON. The body of the request contains the invocation details (the selector to invoke and which arguments to pass).
Every invocation consists of three parts:
When creating a cloud app via Objective-Cloud Connect you have to enter a unique name for your app. This name is used to form the URL for your app. A typically URL looks like https://yourapp.obcl.io
.
The name of the class becomes the path of the URL. As usual in Objective-C a class name has to be unique within your cloud application.
The JSON body is a JSON object (dictionary) with two keys: selector
and arguments
. The selector denotes a string containing the message selector you want to invoke (it is passed, as is, to NSSelectorFromString()
). The arguments is an array with the arguments of the message. The format of the JSON body is described in more detail below.
You can use curl to quickly create and execute HTTP requests. With curl a request for a cloud application with the name yourapp
and a service class called Service
looks like this:
curl -X POST https://yourapp.obcl.io/Service -d '{ "selector" : "sayHello", "arguments" : [] }'
The JSON that forms the HTTP body has to contain an array for the key arguments
. The arguments array contains a list of values passed to the method as arguments.
@protocol ServiceCloudInterface <NSObject>
+ (NSString*)concat:(NSString*)firstPart with:(NSString*)secondPart;
@end
curl -X POST https://yourapp.obcl.io/Service -d '{ "selector" : "concat:with:", "arguments" : [@"Amin", @"Negm"] }'
@protocol ServiceCloudInterface <NSObject>
+ (NSString*)sayHello;
@end
curl -X POST https://YourApp.obcl.io/Service -d '{ "selector" : "sayHello", "arguments" : [] }'
@protocol ServiceCloudInterface <NSObject>
+ (void)methodChangingArg:(inout NSMutableString*)changedArg outArg:(NSString**)outArg;
@end
curl -X POST https://YourApp.obcl.io/Service -d '{ "selector" : "changedArg:outArg:", "arguments" : [@"Hello"] }'
You can pass values typed as JSON classes as usual. You will get the appropriate Cocoa class
JSON Value | Example | Cocoa Class |
---|---|---|
null value | null |
nil , [NSNull null] ¹ |
Boolean true | true |
NSNumber with YES |
Boolean false | false |
NSNumber with NO |
integer number | 9811 |
NSNumber with integerValue |
floating point number | 98.11 |
NSNumber with doubleValue |
string | "9811" |
NSString , NSMutableString ² |
array | [1,2] |
NSArray , NSMutableArray , NSOrderedSet , NSMutableOrderedSet , NSIndexSet , NSMutableIndexSet ² |
object | {name: "Amin", age: 44} |
NSDictionary , NSMutableDictionary ² |
¹ null
is translated into nil
, if it is outside a collection otherwise [NSNull null]
.
² The concrete class depends on the typing of the parameter. If it is id
, an instance of the JSON class is created.
If a parameter type is a custom class, it is transmitted as a dictionary with a key for every property. You should add a key "__com.objective-cloud.objectType"
with a string value containing the class hierarchy as a string of the form "subclass:baseClass:superclass…"
.
This argument class is matched with the class of the parameter. It is possible to send an instance of a subclass to the method, even though the subclass is not known by the server.
Objective-C is a class based programming language. As a basic concept an instance of a derived class is an instance of the base class, too. Therefore you can pass instances of subclasses as arguments to a method, expecting a parameter of a base class.
In Objective-Cloud this concept is respected. Having a method …:
+ (Person*)returnPerson:(Person*)person
{
return person;
}
… and a class Person
with a cloud interface …:
@protocol PersonCloudInterface <NSObject>
@property NSString* firstName;
@property NSString* lastName;
@end
… you can send a request that contains a subclass Employee
of Person
:
curl http://localhost:10000/Service \
-X POST
-d '{"selector" : "returnPerson:", "arguments" : [{ "firstName" : "Amin", "lastName" : "Negm", "salery" : 80000, "__com.objective-cloud.objectType" : "Employee:Person:NSObject" }]}'
This works, even if Employee
is not known on the server side. This makes it easy to store additional data on the client side inside one object instead of splitting it into two objects: one containing the cloud application's state, another one containing the client's state.
The Objective-Cloud runtime system remembers the state of the subclass. If it is returned back to the client, you will get a dictionary for that subclass containing the full state passed to the server.
The response of an invocation consists of an optional return value and optional out arguments. If a request failed, it reports an error or exception.
The response is always a JSON object. It can contain four different key-value pairs:
returnValue
: A return value if the invoked method has a return value (different to void) and successful ran.outArguments
: An array with out arguments, if the method has out arguments. [link]error
: An error, if the runtime system detected an error while invoking the method.exception
: An exception, if the invoked method threw one.Both nil
and [NSNull null]
are transmitted as JSON null.
The type of the return value depends on the class of the returned instance object.
You can return a subclass of the return value's class. In this case the whole state of the subclass' member is returned, the type string contains the subclass, and the client has to handle the subclass' additional key-value pairs.
The out arguments array has one member for every out argument. The typing and coding of these arguments are handled as usual.
If the content or format of a request is invalid then the response contains an error. When we say "error" then we talk about a JSON encoded NSError
. The cloud interface contains a domain
, a code
and a userInfo
dictionary.
$ curl -X POST http://localhost:10000/Service -d '{"selector" : "unknown", "arguments" : []}'
{
"error" : {
"code" : 1407,
"domain" : "com.objective-cloud.foundation",
"userInfo" :
{
"NSLocalizedDescription" : "The selector of an invocation request is unknown."
},
"__com.objective-cloud.objectType" : "NSError:NSObject"
}
}
If a request causes the invoked method to throw an exception, this exception is responded as a JSON object with the keys name
(string), reason
(string), and callStack
(array of string)