![]() |
Magic Internet Kit
|
The Magic Internet Kit takes this concept of a Means object much farther by defining a ConnectableMeans class. This is a Means object mixin that knows how to connect itself to the outside world in some way. Due to the fact that this class is a mixin, it is never instantiated directly, but rather is "mixed in" with existing means objects. The resulting subclasses then implement the methods defined by ConnectableMeans.
All of the Magic Internet Kit's connection classes inherit from ConnectableMeans. For example, the DialupPPPMeans object implements a version of ConnectableMeans that knows how to create TCP/IP connections using PPP over a Magic Cap communicator's built-in modem.
operation CanCreateConnection(): Boolean, noMethod;
This method is called by client code to determine if a connection could potentially be established. For example, if this ConnectableMeans subclass communicates using the modem, CreateConnection could check to see if the phone line is plugged in, make sure the user has set up a dialing location, and also check that the modem is not already in use.
The application calling CanCreateConnection should be aware that the boolean return value is not a guarantee that a connection attempt will succeed. There are many factors that can effect the success of a connection attempt, and many of these cannot be judged until the attempt is actually made. For example, if the object communicates using the modem, and the remote server is not answering, the object won't know that until after dialing. CanCreateConnection therefore should only be counted on as a "sanity check" before doing any real work to try connecting.
operation CreateConnection(): Object, noMethod;
CreateConnection is the method used to connect a ConnectableMeans subclass to a remote host. If the connection attempt succeeds, this method will return the Object ID of the new stream to be used. The stream returned from CreateConnection is always a subclass of CommunicationStream, for example the Modem class for serial modem connections, or the TCPStream class for TCP/IP connections.
If the connection attempt fails, CreateConnection will throw a ConnectException heavyweight exception. Heavyweight exceptions are somewhat different than typical lightweight exceptions used in other parts of Magic Cap, so these will be discussed shortly in the following CreateConnection error handling section.
operation DestroyConnection(stream: Object), noMethod;
DestroyConnection is the inverse of CreateConnection; whenever a stream is no longer needed, this method is used to destroy it in whatever manner is appropriate for the specific ConnectableMeans subclass. This method should also destroy any objects or buffers that were allocated by CreateConnection. For example, if CreateConnection creates a TCPStream object, DestroyConnection will be sure to destroy that object.
With the multitude of possible errors that could happen when connecting, an application using the lightweight exception model would have to catch almost a dozen distinct types of exceptions. The code for just setting up the error handling could be pages long! The Magic Internet Kit takes a slightly different but much cleaner approach by using heavyweight exceptions.
A heavyweight exception is very much like a lightweight exception in how it is used, but in this case the exception is not an error code but rather a real, live object. In the case of CreateConnection, the object that is thrown is a ConnectException or one of its subclasses. Code that is calling CreateConnection, therefore, needs to catch objects of the ConnectException class. The magical method that does this is called, quite reasonably, CatchByClass. CatchByClass will catch all exception objects of a specified class and all of its subclasses. To catch all connect-time errors, catch the ConnectException class. For only errors that deal with hardware, you can catch the HardwareConnectException class, which is a subclass of ConnectException. TCP/IP errors are thrown with TCPConnectException and IPConnectException objects, respectively. Most applications, though, will just want to catch ConnectException since it specifies all of the necessary details about the error in an attribute.
The ConnectException object has an attribute named ConnectError. The value of this attribute is an error code that specifies what exactly went wrong during the connection attempt. The error codes are listed in the MIKErrors.h file in the Magic Internet Kit's "Includes" file. Applications can then check for specific errors, or even check for a category of error by masking off bits. The errors are grouped by hex digit, so to see if the error had something to do with DNS, for example, a simple bitwise AND like the following will do the trick:
if (error & errorDNSMask) /* it's a DNS error */
The first step in this example is creating the objects that we'll need. The ModemMeans object is perfect for what we need to do; it's designed exactly for doing direct serial connections using the modem. For more information on this class, see the Serial Communications in Depth chapter of this document.
The following code defines and creates the ModemMeans object and the Telenumber object that holds the text of the destination number.
ObjectID means = NewTransient(ModemMeans_, nil); ObjectID telenumber = NewTransient(Telenumber_, nil); ObjectID stream; ObjectID exception;
ReplaceTextWithLiteral(Telephone(telenumber), "(800) 555-1212"); SetTelenumber(means, telenumber);
if (!CanCreateConnection(means)) { /* error case */ return false; }
if ((exception = CatchByClass(ConnectException_)) != nilObject) { /* The error code is specified by the ConnectError attribute. */ Unsigned error = ConnectError(exception); /* Figure out the error type */ switch (error) { /* Do what's appropriate here for the errors listed in */ /* MIKErrors.h */ } /* The exception object should be destroyed now that we're done */ /* with it. */ Destroy(exception); return false; }
stream = CreateConnection(means);
Commit(); Write(stream, "hello there!", 12);
DestroyConnection(means, stream); Destroy(means);
ModemMeans inherits from TelephoneMeans and mixes in ConnectableMeans. This class does not add any additional fields, so a typical instance of this class looks the same as a TelephoneMeans instance, for example:
Instance ModemMeans 402; reservationTime: 0; telephoneNumber: (Telenumber 403); End Instance; Instance Telenumber 403; country: 1; extension: nilObject; telephone: (Text 404); End Instance; Instance Text 404; text: '(800) 555-1212'; End Instance;
Instance SerialPortMeans 452; baudRate: 38400; End Instance;
The Magic Internet Kit provides three classes for communicating via TCP: DialupPPPMeans, MagicBusPPPMeans, and XircomMeans. Each of these classes will be discussed briefly, and for more information see the Reference section of this guide.
DialupPPPMeans inherits from DialupIPMeans, ConnectableMeans, and UsesTCP. DialupIPMeans is a built-in Magic Cap class that contains fields for useful information, namely the remote phone number, host name, and TCP port. ConnectableMeans, as described above, adds on the single-API interface for creating a destroying connections. The UsesTCP class is provided in MIK to do the hardware independent work of managing data link servers and TCP streams. UsesTCP will be described later in the Behind the scenes with TCP/IP section.
Instances of TCP-based Means objects contain a bit more information than the serial-based Means objects that were shown above. You'll notice that some parts are the same as for ModemMeans, namely the Telenumber and Text object. There are a few additional classes, though: a FixedList and Monitor. These will be discussed in greater detail as part of the UsesTCP mixin class. Here's a sample instance of DialupPPPMeans and its supporting classes:
Instance DialupPPPMeans 502; reservationTime: 0; telephoneNumber: (Telenumber 503); hostName: (Text 505); port: 80; linkAccess: (Monitor 508); linkClass: PPPLinkServer_; meansSourceIP: 0; dNSServers: (FixedList 509); login: (Text 506); password: (Text 507); End Instance; Instance Telenumber 503; // 12 bytes country: 1; extension: nilObject; telephone: (Text 504); End Instance; Instance Text 504; text: '(800) 555-1212'; End Instance; Instance Text 505; text: 'www.genmagic.com'; End Instance; Instance Text 506; text: 'none'; End Instance; Instance Text 507; text: 'none'; End Instance; Instance Monitor 508; next: nilObject; count: 1; user: nilObject; End Instance; /* List of DNS servers */ Instance FixedList 509; length: 1; entry: 0x12345678; End Instance;
XircomMeans, like DialupPPPMeans, inherits from DialupIPMeans, ConnectableMeans, and UsesTCP. The DialupIPMeans inheritance is not totally appropriate since this is not a dial-up connection, but the fields for the remote host name and port are required by the TCP/IP libraries. The Telenumber attribute is ignored and should be set to nilObject.
UsesTCP is the mixin class that all of the above TCP/IP Means classes inherit from to do the gnarly work of managing TCP/IP streams and data link servers. UsesTCP knows how to automagically connect and disconnect the link servers when needed, resolve host names with DNS when they are specified symbolically, and other fun stuff. We'll take a look at the features of UsesTCP, and you can find complete documentation on its implementation in the TCP/IP Communications in Depth chapter.
First of all, TCP/IP has the neat capability of creating multiple communications streams over the same data link. This would usually present a bit of a problem, though, when it comes to connecting and disconnecting streams because the code would have to figure out when it needs to create or destroy the link server. UsesTCP does this automatically by keeping count of the number of streams that a given link server has running on it. Client code, therefore, does not have to worry about managing the data link.
Second, the names of IP destinations can be specified by either an IP address, e.g. 192.216.22.142, or symbolic host name, e.g. www.genmagic.com. UsesTCP will look at the HostName attribute of the Means object, and if the name is not an IP address, it will ask the DNS servers in the DNSServers attribute if they can turn the name into an IP address.
A third feature of UsesTCP is its flexibility. UsesTCP provides methods that subclasses can override to do custom initialization of the data link server and do any negotiation with the remote host before the link level protocol is activated. Additionally, client code can manually open and close the data link level without creating a TCP/IP stream at the same time. This is useful if the client code wants to create and destroy several streams, one after another, without opening and closing the data link each time.
For more information on UsesTCP, see the TCP/IP Communications in Depth chapter and the Reference section of this document.
Select TCP/IP as your connection type, and then add whatever hardware support you desire. Also include DNS if you want name resolution capabilities. I'll name my new application "Finger." Now build the new application for the Macintosh simulator.
Figure 5 Finger user interface
With these objects dumped out, go back to MPW and paste them into your Objects.Def file as you would with any other Magic Cap application.
Define Class FingerButton; inherits from Button; field queryText: Object, getter, setter; field replyText: Object, getter, setter; attribute QueryText: Object; attribute ReplyText: Object; overrides Action; End Class;
Instance FingerButton 'Do it!' 9; next: (TextField 'Server reply:' 10); previous: (SimpleActionButton 'Set up host info' 17); superview: (Scene 'Finger' 6); subview: nilObject; relativeOrigin: <66.5,-43.0>; contentSize: <50.0,24.0>; viewFlags: 0x70101200; labelStyle: iBook12Bold; color: 0xFF000000; altColor: 0xFF000000; shadow: nilObject; sound: iTouchSound; image: nilObject; border: iSquareButtonBorderUp; queryText: (TextField 'User to finger:' 2); replyText: (TextField 'Server reply:' 10); End Instance;
Method void FingerButton_Action(ObjectID self) { ObjectID means = DirectID(iiMeans); ObjectID replyText = ReplyText(self); ObjectID queryText = QueryText(self); ObjectID stream; ObjectID exception; Unsigned count; Boolean done; Str255 replyString; Str255 queryString; /* Clear the reply TextField */ DeleteText(replyText); /* ** Make sure the Means object is TCP-capable! iiMeans refers to ** the current Means object that the user set up in the "Finger ** Setup" scene included with the template that we used ** (EmptyWithFrills). See ConnectChoiceBox and HardwareChoiceBox's ** SetLevel methods for the implementation of this. */ Assert(Implements(means, UsesTCP_)); /* Set the port to 79 (the finger port) */ SetPort(means, 79); /* If we fail the first sanity-check, return immediately. */ if (!CanCreateConnection(means)) { Honk(); ReplaceTextWithLiteral(replyText, "Ack! Can't connect!"); return; } /* Catch all connect-time exceptions. */ if ((exception = CatchByClass(ConnectException_)) != nilObject) { /* An error occured while connecting! We'll just return. */ Honk(); ReplaceTextWithLiteral(replyText, "Ack! An error occured while trying to connect!"); /* Destroy the exception object now that we're done with it. */ Destroy(exception); return; } /* Connect to the remote host! */ stream = CreateConnection(iiMeans); /* Commit the above exception handler */ Commit(); /* Send the query to the remote host */ TextToString(queryText, queryString); if (Catch(serverAborted) != nilObject) { /* An error occured while writing! */ Honk(); ReplaceTextWithLiteral(replyText, "Ack! An error occured while trying to write!"); return; } Write(stream, &queryString[1], queryString[0]); Write(stream, "\xD""\xA", 2); Commit(); /* Read in the reply */ done = false; while (!done) { char* currentChar; /* Read up to 255 characters at a time */ count = Read(stream, replyString, 255); replyString[count] = 0x0; /* Null-terminate the string */ /* Turn those pesty carriage returns into spaces */ currentChar = replyString; while (currentChar[0]) { if (currentChar[0] == 0xd) currentChar[0] = 0x20; currentChar++; } /* Stick the data into the "server reply" TextField */ AppendLiteral(replyText, replyString); if (count < 255) done = true; } /* Destroy the connection now that we're done. */ DestroyConnection(means, stream); }
Assert(Implements(means, UsesTCP)); SetPort(means, 79); if (!CanCreateConnection(means)) return;
Once that's done, we'll call our sanity-check method CanCreateConnection to see if we can possibly connect. If not, we'll return immediately.
if ((exception = CatchByClass(ConnectException_)) != nilObject) { Honk(); Destroy(exception); return; }
stream = CreateConnection(means); Commit();
if (Catch(serverAborted) != nilObject) { /* An error occured while writing! */ Honk(); ReplaceTextWithLiteral(replyText, "Ack! An error occured while trying to write!"); return; } Write(stream, &queryString[1], queryString[0]); Write(stream, "\xD""\xA", 2); Commit();
With our error handling code in place, we can try to stuff some bytes down the TCP stream. The first Write call sends the user name to the server, and the second Write sends a carriage return/linefeed combo that terminates the query. After this is done, we can Commit the above exception handler since we are done with the code that could fail.
count = Read(stream, replyString, 255); (...) AppendLiteral(replyText, replyString);
DestroyConnection(means, stream);
In Magic Cap, a thread is called an Actor. Applications wishing to create their own actors must subclass the Actor class and override its Main method to make it do what they want. This section will briefly cover how to use actors, but Magic Cap Concepts provides a far more complete discussion of this topic. We recommend that you read that chapter as soon as you have time.
Magic Cap uses cooperative multitasking with its actors, so each actor should be sure to give up time to other actors. This is done by calling RunNext on the scheduler, referenced by the iScheduler indexical. Calling RunNext tells the scheduler to run the next waiting actor.
Some methods that you might call will automatically call RunNext if they are going to take a while to return. For example, if you call Read on a TCPStream object, and the Read cannot be immediately satisfied because all the bytes are not available, Read will call RunNext to let other actors do their stuff.
newActor = NewTransient(CommsActor_, nil);
NewActorParameters newActorParams; ZeroMem(&newActorParams, sizeof(NewActorParameters)); SetUpParameters(&newActorParams.header, newActorObjects); newActorParams.stackSize = 0x2000; /* default is 0x1000 (4K) */ newActor = NewTransient(CommsActor_, &newActorParams.header);
Method void MyActor_Main(ObjectID self, Parameters* UNUSED(params)) { while (FeelingPeachy(self)) { DoSomeStuff(self); if (SomeFatalErrorOccurred(self)) { return; } } }
There is one more interesting caveat to using actors in Magic Cap: an actor is a transient object, so in Magic Cap 1.0 a power cycle would destroy the actor. This is not always that case in Magic Cap 1.5, so special care must be taken to ensure that the state of an actor does not get confused if the power is turned off and then back on. This is particularly important with communications, of course, because any data links will get shut down when the power is turned off.
The key to managing transient actors in Magic Cap 1.5 is overriding the Reset method of an object. Reset gets called when the device is powered on, so you can use this method to hunt down any actors that need to be managed and do whatever is appropriate. For communications this usually means destroying the actor.
There are two key methods in the Semaphore class that you should be aware of: Access and Release. Access is used to "hold down" the semaphore. If the semaphore being accessed is not already held down by code on another actor, Access will return immediately. If the semaphore is already accessed, though, Access will block until someone else releases the semaphore using Release. Release will tell the semaphore that you're done messing with it, and it will then wake up the next actor in the queue, if any, that wanted to access the semaphore.
Every actor has its own exception stack, so an exception thrown on one actor using Fail cannot be caught from another actor. This is very useful for localizing exception handling code; for example a serverAborted exception thrown on a communicating application's comms actor won't be caught accidentally by the Post Office actor in the system.
If code executing on one actor wants to throw an exception on a different actor, it should use the FailSoon method. FailSoon will cause a specified exception to get thrown on the other actor the next time that the actor comes up in the scheduler. If the target actor is not ready to run, e.g. it is blocked, the actor will be awakened and the exception thrown.
Usage of this method is most easily explained with an example:
typedef struct { Parameters header; ObjectID interestingThing; } DoSomethingUsefulParams; #define numObjectsDoSomethingUsefulParams 1 Private void DoSomethingUseful(DoSomethingUsefulParams *params) { ObjectID thing = params->interestingThing; DestroyTransientBuffer(params); DoSomethingElse(thing); } Method void Thing_DoSomethingElse(ObjectID self) { // stuff... } #undef CURRENTCLASS #define CURRENTCLASS MyActor Method void MyActor_Main(ObjectID self, Parameters* UNUSED(params)) { DoSomethingUsefulParams* params; // other stuff... params = NewTransientBuffer(sizeof(*params)); SetUpParameters(¶ms->header, numObjectsDoSomethingUsefulParams); params->interestingThing = ThingObject(self); RunSoon(true, (CompletionFunction) DoSomethingUseful, ¶ms->header); // other stuff... }
typedef struct { Parameters header; ObjectID interestingThing; } DoSomethingUsefulParams; #define numObjectsDoSomethingUsefulParams 1
Private void DoSomethingUseful(DoSomethingUsefulParams *params) { ObjectID thing = params->interestingThing; DestroyTransientBuffer(params); DoSomethingElse(thing); }
Method void Thing_DoSomethingElse(ObjectID self) { // stuff... }
Method void MyActor_Main(ObjectID self, Parameters* UNUSED(params)) { DoSomethingUsefulParams* params; params = NewTransientBuffer(sizeof(*params)); SetUpParameters(¶ms->header, numObjectsDoSomethingUsefulParams); params->interestingThing = ThingObject(self); RunSoon(true, (CompletionFunction) DoSomethingUseful, ¶ms->header); }