Mac to Mainframe
Volume Number: | | 6
|
Issue Number: | | 6
|
Column Tag: | | C Workshop
|
Mac to Mainframe With HyperCard
By John R. Powers, III, Monte Sereno, CA
Note: Source code files accompanying article are located on MacTech CD-ROM or source code disks.
[John Powers designs and programs computer software tools for people who are not computer experts. In his 23 years of software development experience, Mr. Powers has developed products ranging from portable computers for business executives to educational games for children. Mr. Powers has been employed by Battelle-Columbus Laboratories, the Authorship Resource Inc., Atari, Convergent, and the Learning Company. In 1988, he formed Easy Street Software. His most recent projects have been developing software for the Mac-to-NonMac connection using C and HyperTalk.]
Trapping the Wild XFCN
Little did I know that the phone call would lead me on an adventure with the biggest and wildest XFCNs Ive ever done. It all started when Tri-Data Systems wanted to do something special with their Mac-to-Mainframe connection.
The Mac-to-Mainframe Connection
Mac communications with IBM mainframes has become one of the hot market areas for the Macintosh. Large corporate users of IBM mainframes have discovered the Macintosh and want to use the Macintosh with their mainframes. They try the terminal emulators on their Macintosh for the mainframe tasks that they have always done, but they become frustrated that they cant enjoy the full power and flexibility of the Macintosh interface. They enjoy using HyperCard for their other tasks and want to use HyperCard for their mainframe communication as well. Mainframe users are especially frustrated when they want to customize their interaction with the mainframe and cant use the capability of HyperCard. They were asking major communication hardware and software suppliers like Tri-Data Systems why they couldnt use HyperCard.
The HyperCard-to-Mainframe Connection
Tri-Data Systems had completed a Mac-based programmatic interface to IBM mainframes and wanted to extend that capability to HyperCard. Using a HyperCard stack, a user should be able to use or write handlers through which a stack could communicate with an IBM mainframe. Traditional communication methods require a dedicated terminal emulator or a custom application program, but Tri-Data Systems wanted more: a HyperCard interface that would greatly simplify the development of custom interfaces by giving the user a powerful set of HyperCard tools combined with extensions for IBM mainframe communication. Handlers could be written that provide a one-button logon and use HyperCard fields to store and retrieve mainframe data. Complete transactions could be automated using HyperCard.
The HyperCard XCMD/XFCN capability was the logical way to extend HyperCard into the arena of IBM mainframe communication. The XCMD/XFCN interface would be the linkage between HyperCard and Tri-Datas driver. This relationship is shown in figure 1, The HyperCard-to-Mainframe Connection.
Figure 1.
A number of HyperCard interfaces to IBM mainframe communication were already on the market, but didnt have the power that Tri-Data Systems wanted for their implementation. Tri-Data Systems wanted the works:
a complete user-programmable interface to IBM mainframes;
the support of up to eight separate and simultaneous interaction sessions in a single stack;
synchronous and asynchronous operation;
dynamic memory allocation to minimize memory overhead;
access to internal or external keyboard mapping tables;
a complete set of 138 result codes with optional text describing each code;
full support for HyperCard chunking expressions; and
a stack demonstrating all 15 commands
The user-programmable interface had to support 15 commands dealing with IBM mainframe communication. Commands include those for setting up and closing down communication with the mainframe, passing data back and forth, and dealing with cursor information. The commands had to be powerful enough to create a terminal emulator in HyperCard alone.
The capability of separate and simultaneous sessions had to allow the user to manage up to eight independent interactions with the mainframe at one time, all operating in one stack. Each session had to operate independently and have its own queues, buffers, and request blocks, all requiring memory. However, Tri-Data Systems did not want to penalize the user that wanted fewer sessions with excessive memory requirements, memory was to be allocated in a dynamic fashion on an as-needed basis.
Synchronous and asynchronous operation would allow the user to choose between the operation being completed while they wait or permitting the user to do something else in the stack while the operation was completed. Synchronous operation is the more common of these two modes, you initiate the command and wait for the result. Asynchronous operation allows you to initiate the command and proceed immediately with something else without waiting for the result. The process completes occurs while you are potentially elsewhere in the stack.
Mainframe interaction may require non-standard keyboard mappings. Tri-Data Systems wanted to be sure that any special keyboard mappings could be specified from within the HyperCard stack or with a resource attached to the stack.
A lot of messages in the form of result codes can be passed back to the user. These can be from various levels in the driver or XFCN. Manuals for mainframe communication software usually always have one or more appendices containing pages of result codes. Tri-Data Systems wanted more than just a code, they wanted the user to see a textual description of the code at the users option.
In addition to these major features, the HyperCard interface had to support full chunking expressions such as char 21 to 40 of line 7 of bg fld PS and provide a large variety of initialization options with defaults.
Finally, the product was to be bundled in a stack which would demonstrate the operation of each of the 15 commands.
It was a big list of requirements and the foremost requirement was the HyperTalk interface.
The HyperTalk Interface
My first step was to design a user interface that would accommodate all the Tri-Data Systems requirements. A natural hierarchy occurs in the design, one connection method is used for all sessions and each session has its own set of commands (shown in figure 2, System Hierarchy). This hierarchy is followed both in the HyperTalk interface and the internal data structures. As a result, there are three major classes of data:
Figure 2.
Connection method data
Connection initialization data specifies connection method, translation tables, and the number of sessions desired. This is done only once.
Session data
Session initialization data is passed to the XFCN.
Session status data is received from the XFCN and driver.
Command
The command name and the session is passed. Command-specific options is also passed and command-specific status information is received.
Container descriptions (chunking expressions) for passing and receiving mainframe are passed.
Results of the transaction is received. Was the command completed successfully? Is it still pending (for asynchronous operations)? Did it fail and why?
As to the passing of data between stack and XFCN, a combination of HyperCard globals, parameters lists, and function results supports the three classes of data nicely:
Globals Used for connection method and session data.
Parameter list Passes command name, command options, and container descriptions used by the command.
Function result Returns the result code and command-specific data.
In addition, HyperCard fields are the most likely choice for storing and displaying data that the command passes between the mainframe and HyperCard. HyperCard globals can also be used, but if the data is used for display, putting it in a field to begin with is more efficient.
Two types of globals maintain parametric data needed by HyperTalk and the XFCNs. The method global contains the connection method parameters and the session global contains the session-specific parameters. There is one method global for the stack and one session global for each simultaneous session. The only mandatory data in the method global are the names of the session globals, if no additional data is provided, the XFCN uses its defaults. The session globals are two-way. That is, initialization data is passed to the interface in the global and the interface passes session status information back to the global. The user can optionally inspect the session global to see connection status, the connection id, and so forth.
The parameter lists have the command name as their first parameter and the session global name as the second parameter. The session global name is required to identify which session of multiple simultaneous sessions is being referenced.
The function result for the XFCN returns the result code. If the command returns additional information, such as a cursor location, it is returned in a comma-separated list following the result code.
Operations are separated into three functions: one to launch a command, another to check a command state, and a third to translate the result code into a text string. The resulting XFCNs are as follows:
TriData3270 Launches command and returns its result code.
TriData3270State Returns a result code giving the state of a command that was launched asynchronously.
TriData3270Result Returns the text string corresponding to a result code.
TriData3270 is the workhorse since it launches all the commands while the other two XFCNs provide specialized support.
The HyperTalk interface is illustrated in the following handlers:
--1
--
-- STARTUPSESSION function
-- Startup the session by an INIT, OPEN, and CONNECT.
-- Return true if startup was done without error.
-- If any error occurs, terminate and return false.
--
-- Script byJohn R. Powers, III
-- Easy Street Software
--
-- XFCNs used: TriData3270
-- XCMDs used: none
-- HANDLERS used: setupGlobals, showResult,
updateDebug
-- FUNCTIONS used: none
--
function startupSession
setupGlobals
get TriData3270(INIT_3270_API, myMethod)
showResult(it)
if item 1 of it 0 then return false
get TriData3270(OPEN_HOST_CONNECTION,¬
myMethod)
showResult(it)
if item 1 of it 0 then
get TriData3270(TERM_3270_API)
return false
end if
get TriData3270(CONNECT_TO_PS,¬
mySession, sync, )
showResult(it)
if item 1 of it 0 then
get TriData3270(TERM_3270_API)
return false
end if
updateDebug
return true
end startupSession
----------------------------------------------
--
-- GET_UPDATE button
-- Initiate an async Get_Update for PS and OIA.
-- Handlers are in the background script.
--
-- Script byJohn R. Powers, III
-- Easy Street Software
--
-- XFCNs used: TriData3270
-- XCMDs used: none
-- HANDLERS used: showResult, updateStatus
-- FUNCTIONS used: none
--
on mouseUp
global TD3270temp
showResult(TriData3270(GET_UPDATE,¬
mySession,async,50",¬
cd fld PS,,,,cd fld OIA))
updateStatus
end mouseUp
----------------------------------------------
--
-- CHKUPD button
-- Check the state of Get_Update.
-- This forces an update of the fields passed
-- in the Get_Update command.
-- Handlers are in the background script.
--
-- Script byJohn R. Powers, III
-- Easy Street Software
--
-- XFCNs used: TriData3270State
-- XCMDs used: none
-- HANDLERS used: showResult, updateStatus
-- FUNCTIONS used: none
--
on mouseUp
global TD3270temp
showResult(TriData3270State(GET_UPDATE,¬
mySession))
updateStatus
end mouseUp
----------------------------------------------
--
-- SHOWRESULT function
-- Posts the return from the TriData3270 function.
-- Separates the result code (always item #1).
-- Gets the result message from the result code
-- and posts both to the display.
-- Also displays the mySession global since
-- TriData3270 may have updated it.
--
-- Script byJohn R. Powers, III
-- Easy Street Software
--
-- XFCNs used: TriData3270Result
-- XCMDs used: none
-- HANDLERS used: showResult
-- FUNCTIONS used: none
--
on showResult resultInfo
global mySession
-- flash to show update
put empty into cd fld resultCode
-- flash to show update
put empty into cd fld resultMessage
put resultInfo into cd fld resultCode
put TriData3270Result(item 1 of resultInfo)¬
into cd fld resultMessage
put mySession into cd fld Session
end showResult
----------------------------------------------
Code Requirements
The full-featured capability and flexibility required by Tri-Data Systems provided some challenges for the code design.
Data storage is allocated upon entering the XFCN, but also must be maintained after exiting the XFCN. This is especially important because asynchronous operation requires stable data structures that were accessible by the driver after the XFCN completes execution. Since the XFCN goes away when not executing, no globals or other static memory areas could be used. Data structures needed to be preserved between XFCN calls.
The TriData3270 and TriData3270State XFCNs share a lot of functionality. Duplicating this functionality means duplicate code, wasted memory, and complicated software maintenance. The XFCNs needed to share common code.
Tri-Data Systems wanted support for complete chunking expressions, not just container names. Users could enter char 2 to 10 of line 5 of bg fld PSdata rather than just PSdata for container descriptions. Mimicking HyperCards powerful chunking expression processing is a big coding job and wasteful. Complete chunking expressions needed to be supported.
Up to 8 simultaneous sessions are possible, each with its own memory allocation consisting of a variety of request blocks, queue blocks, buffers, translation tables, and record blocks. Some of this memory is fixed length and some of it is variable length depending on how much data the user is transferring or what options the user has selected. Allocating all this memory at initialization for all possible sessions would exceed the capacity of most Macintosh computers. Dynamic memory allocation was needed.
Tri-Data Systems required development to be in MPW 3.0. Unfortunately, the SADE debugger does not support debugging of XCMDs or XFCNs. In addition, successful debugging of communication software requires the ability to trace data as it passes thru the data structures used by the code being analyzed. Its not sufficient to just look at the input and output. A new debugging tool was needed.
Preserving Data Structs Between XFCN Calls
A single memory handle is allocated for the entire connection. Additional memory handles, and there are many, are referenced from the single memory handles structure (further described in Dynamically Allocating And Preserving Memory). To preserve data structures between XFCN calls, the memory handle is stored as a string in a HyperCard global. This is shown in Figure 3, Preserving Data Between Calls. The handle could not be saved in a HyperCard global in its native form because of the possibility of a zero byte being misinterpreted by HyperCard as a string terminator. To avoid this possibility, each byte of the memory handle is translated into an ASCII code so that the result can be displayed in HyperCard as a string of hex characters. Storing the handle as a hex string also helps in debugging.
Figure 3.
The following functions are used to convert, save, and retrieve the memory handle:
/* 2 */
int storeHanInGlobal(paramPtr, globalName, han)
/*
Convert the handle to HexStr representation.
Store in the HC global container globalName.
If handle is zero, store empty in the global.
This lets the HyperCard script test to see if
memory has been allocated (not empty ==
allocated).
Returns result code.
*/
XCmdPtr paramPtr;
char *globalName;
Handle han;
{
Handle tempHan;
Str255 tempPstr;
char hanCstr[kMaxLenLine];
if(han==NIL)
strcpy(hanCstr, kEmptyStr);
else
convertHanToHexStr(hanCstr, han);
copyCtoPstr(tempPstr, globalName);
tempHan = CopyStrToHand(hanCstr);
SetGlobal(paramPtr, tempPstr, tempHan);
DisposHandle(tempHan);
return MemError();
}/*--------------storeHanInGlobal */
int fetchHanFromGlobal(paramPtr, globalName,
han)
/*
Fetch the handle stored in HexStr
in the HC global container globalName.
Returns result code:
TRUE successful, found handle
FALSE handle is NIL
*/
XCmdPtr paramPtr;
char *globalName;/* name of global */
Handle *han; /*pointer to handle */
{
Handle hGlobalString;/* string in global*/
Str255 tempPstr;/*temp Pascal string */
copyCtoPstr(tempPstr, globalName);
HLock(hGlobalString = GetGlobal(paramPtr, tempPstr));
convertHexStrtoHan(han, *hGlobalString);
HUnlock(hGlobalString);
if(*han==NIL)
return FALSE;
else
return TRUE;
}/*--------------fetchHanFromGlobal*/
void convertHanToHexStr(hexStr, han)
/*
Convert the Handle to 8-char (+ terminator) hex string representation.
*/
char *hexStr;
Handle han;
{
short digit, shiftCnt = 32;
do
{
shiftCnt -= 4;
digit = 0 + (((short) ( (long) han >>
shiftCnt)) & 0x000F);
if(digit > 9)
digit += 7;/* A-F hex digit*/
*hexStr++ = digit;
}
while(shiftCnt>0);
*hexStr = \0; /*add terminator */
}/*--------------convertHanToHexStr*/
void convertHexStrtoHan(han, hexStr)
/*
Convert the hex string representation to a Handle.
The HexStr representation must be a C-string.
Only the least-significant 8 hex digits are used.
Assumes that handle is already locked.
Returns NIL for handle if there are not 8 hex digits.
*/
Handle *han;
char *hexStr;
{
unsigned long digit, hanVal=0L;
short shiftCnt=0, i;
Str255 tempPstr;
if(strlen(hexStr)!=8)
{
*han = NIL;
return;
}
copyCtoPstr(tempPstr, hexStr);
for(i=tempPstr[0]; i>0 && shiftCnt < 32; i--)
{
digit = tempPstr[i] - 0;/*unsigned, pos*/
if(digit>9L)
digit -= 7L; /*A-F hex digit */
if(digit>F)
{
*han = NIL;
return;
}
digit <<= shiftCnt; /* shift left */
hanVal = hanVal | digit;/* insert value*/
shiftCnt += 4;
}
*han = (Handle) hanVal;
}/*--------------convertHexStrtoHan*/
GetGlobal and SetGlobal are callbacks supported by HyperCard. They are simple and convenient to use. GetGlobal passes the HyperCard parameter block and the names of the global as a Pascal string. It returns a handle to the content of the global. SetGlobal passes the HyperCard parameter block, the name of the global as a Pascal string, and a handle to the content to be stored in the global. The global contents are passed as null-terminated C-strings.
Sharing Common Code
A variety of approaches are possible for sharing common code. One approach would be to combine the TriData3270 and TriData3270State functions into a single XFCN. This would complicate the parameter list and may confuse the user. A better approach is to have the two XFCNs call a common code module. TriData3270 and TriData3270State were written as two very small XFCNs calling a common CODE resource. The XFCNs pass the HyperCard parameter block and a parameter indicating which XFCN is calling the common CODE resource. The common CODE resource uses the who called me parameter to switch between unique and common code.
The common CODE resource is accessed as a named resource by GetNamedResource. It seems less likely that the CODE resource name will be changed than its number. If GetNamedResource results in an error code, then an appropriate result code is returned to the calling stack. Since the convention for this series of XFCNs is to return the result code in hex, the error code is converted to a hex string and returned. To keep things compact, the makeHexString function is duplicated between the two XFCNs. This small duplication is only required to support the error condition. The CODE resource handle is cast as a ProcPtr and called as a function after being moved high and locked. After execution, the handle is unlocked.
The complete XFCNs and a fragment of the entry into the common CODE resource are as follows:
/* 3 */
/* ----------------------------------------------
Program: TriData3270 XFCN
Version: v2
History: 05/04/89 v1, original
07/04/89 v2, revised memory model
Author:John R. Powers, III
Easy Street Software
16373 Alexander Avenue
Monte Sereno, CA 95030
(408) 395-1158
(408) 395-3308 msg.
Computer:Mac II with System v6.0.2
Compiler:MPW-C v3.0
Usage:
get TriData3270(command, session, SAM,
params, container)
Requires HyperCard version 1.2 or better.
Files:
TriData3270.c This source file
TD3270main.c CODE called by
TriData3270State
TriData3270 Lab HyperCard stack for testing
TriData3270.h General header
Full Build:
# TriData3270 XFCN
C -b TriData3270.c -mbg off -sym off
Link
-rt XFCN=16373
-m ENTRYPOINT
-sg TriData3270
TriData3270.c.o
{Libraries}Interface.o
{CLibraries}StdCLib.o
-o TriData3270 Lab
save -a
---------------------------------------------- */
#include<HyperXCmd.h>
#include<Memory.h>
#include<Types.h>
#include<Resources.h>
#includeTriData3270.h
/*
Prototypes.
*/
void makeHexStr(char *hexStr, short value);
void pTD3270main (XCmdPtr paramPtr, short opFlag);
/*
Main program and entry point for XFCN.
Get common code resource and continue with that.
Use opFlag = kOp3270 (TriData3270 XFCN).
*/
pascal void entryPoint(paramPtr)
XCmdPtr paramPtr;/*the HyperCard connection*/
{
short opFlag=kOp3270, result;
char msg[kMaxLenLine];
Handle hMsg, hTD3270main;
ProcPtrpTD3270main;
hTD3270main = GetNamedResource(kRsrcMainType,
kRsrcMainName);
result = ResError();
if(result!=noErr)
{
makeHexStr(msg, result);
hMsg = (Handle) NewHandle((long) (strlen(msg)+1));
HLock(hMsg);
strcpy(*hMsg, msg);
HUnlock(hMsg);
paramPtr->returnValue = hMsg;
return;
}
MoveHHi(hTD3270main);
HLock(hTD3270main);
pTD3270main = (ProcPtr) *hTD3270main;
pTD3270main (paramPtr, opFlag);
HUnlock(hTD3270main);
return;
}/*-------------------- entryPoint */
void makeHexStr(hexStr, value)
/*
Convert the value to 4-char (+ terminator) hex string representation.
*/
char *hexStr;
short value;
{
short digit, shiftCnt = 16;
while(shiftCnt>0);
{
shiftCnt -= 4;
digit = 0 + ((value>>shiftCnt) & 0x000F);
if(digit > 9)
digit += 7;/* A-F hex digit*/
*hexStr++ = digit;
}
*hexStr = \0; /*add terminator */
}/*---------------------- makeHexStr */
/* ----------------------------------------------
Program: TriData3270State XFCN
Version: v1
History: 07/04/89 v1, original
Author:John R. Powers, III
Easy Street Software
16373 Alexander Avenue
Monte Sereno, CA 95030
(408) 395-1158
(408) 395-3308 msg.
Computer:Mac II with System v6.0.2
Compiler:MPW-C v3.0
Usage:
get TriData3270State(command, session)
Requires HyperCard version 1.2 or better.
Files:
TriData3270State.cThis source file
TD3270main.c CODE called by TriData3270State
TriData3270 Lab HyperCard stack for testing
TriData3270.h General header
Full Build:
# TriData3270State XFCN
C -b TriData3270State.c -mbg off -sym off
Link
-rt XFCN=16374
-m ENTRYPOINT
-sg TriData3270State
TriData3270State.c.o
{Libraries}Interface.o
{CLibraries}StdCLib.o
-o TriData3270 Lab
save -a
---------------------------------------------- */
#include<HyperXCmd.h>
#include<Memory.h>
#include<Types.h>
#include<Resources.h>
#includeTriData3270.h
/*
Prototypes.
*/
void makeHexStr(char *hexStr, short value);
void pTD3270main (XCmdPtr paramPtr, short opFlag);
/*
Main program and entry point for XFCN.
Get common code resource and continue with that.
Use opFlag = kOp3270State (TriData3270State XFCN).
*/
pascal void entryPoint(paramPtr)
XCmdPtr paramPtr;/*the HyperCard connection*/
{
short opFlag=kOp3270State, result;
char msg[kMaxLenLine];
Handle hMsg, hTD3270main;
ProcPtrpTD3270main;
hTD3270main = GetNamedResource(kRsrcMainType,
kRsrcMainName);
result = ResError();
if(result!=noErr)
{
makeHexStr(msg, result);
hMsg = (Handle) NewHandle((long) (strlen(msg)+1));
HLock(hMsg);
strcpy(*hMsg, msg);
HUnlock(hMsg);
paramPtr->returnValue = hMsg;
return;
}
MoveHHi(hTD3270main);
HLock(hTD3270main);
pTD3270main = (ProcPtr) *hTD3270main;
pTD3270main (paramPtr, opFlag);
HUnlock(hTD3270main);
return;
}/*-------------------- entryPoint */
void makeHexStr(hexStr, value)
/*
Convert the value to 4-char (+ terminator) hex string representation.
*/
char *hexStr;
short value;
{
short digit, shiftCnt = 16;
while(shiftCnt>0);
{
shiftCnt -= 4;
digit = 0 + ((value>>shiftCnt) & 0x000F);
if(digit > 9)
digit += 7;/* A-F hex digit*/
*hexStr++ = digit;
}
*hexStr = \0; /*add terminator */
}/*---------------------- makeHexStr */
/*
main program and entry point
The is intended to be called as a CODE
resource, not an XCMD or XFCN.
Call with opFlag==kOp3270 for TriData3270
functionality.
Call with opFlag==kOp3270State for
TriData3270State functionality.
TriData3270State is a subset of TriData3270.
There is so much common code that this
approach eliminates redundant source and
object code.
*/
void entryPoint(paramPtr, opFlag)
XCmdPtr paramPtr;/*the HyperCard connection*/
short opFlag;
{
masBlk *pMas; /*master memory pointer */
sesBlk *pSes;
Handle hMas, /*master block handle */
hSes;
char sessionGlobalName[kMaxLenName],
command[kMaxLenCmd],/* command string*/
returnList[kMaxLenRet];/*return strng*/
short result, /*result code for return */
ignore,
debugFlag,/* debug flag */
reqResult,/* result code from API*/
status,/*verb queue status code*/
cmdNum,/*command number */
blockIndx,/* queue/request blk indx*/
numParams,/* number of XFCN parameters*/
sessionIndx;
WORD wIgnore;
/*Map command index to */
/*req and queue indexes */
short blockIndxTable[] =
{
kIndxConnPS, /*ConnPS */
kIndxCpyToPS, /*CpyToPS */
kIndxDiscPS, /*DiscPS */
kIndxGetCurs, /*GetCurs */
kIndxSendKey, /*SendKey */
kIndxSetCurs, /*SetCurs */
kIndxCpyfBuf, /*CpyfBuf */
kIndxGetUpd, /*GetUpd */
kIndxCloseHC, /*CloseHC (no queue)*/
kIndxOpenHC, /*OpenHC (no queue) */
kIndxSendKey, /*SendAID => SendKey*/
kIndxSendKey, /*SendText => SendKey */
kIndxInit, /* no queue & no request */
kIndxTerm/*no queue & no request */
};
/*
Setup return string.
Check for opFlag validity.
*/
*returnList = kCterm;
if(opFlag!=kOp3270 && opFlag!=kOp3270State)
{
resultForHC(paramPtr, kRcHuhOp, returnList);
return;
}
/*
Supporting Complete Chunking Expressions
Containers, particularly fields, are used extensively in the Mac-to-Mainframe connection. I wanted to support complete chunking expressions so that the user could refer to chunks of the container rather than the container in its entirety. It was silly to mimic HyperCards powerful chunking capability so the solution was to let HyperCard do it.
When the user passes a container description to the XFCN, a callback is formed which has HyperCard interpret the description and place the data into a simple container. The simple container is then processed by the XFCN. For example, if the user wants to send char 21 to 40 of line 6 of cd fld PSdata to the mainframe, a callback is created as follows:
Put <users chunking expression> into <temporary global>
forming
Put char 21 to 40 of line 6 of cd fld PSdata into TD3270temp
The callback lets HyperCard process the statement and perform the data transfer from the users container to the temporary global, TD3270temp. This allows the XFCN to manipulate the simple container, TD3270temp, rather than try to interpret the more complex chunking expression, char 21 to 40 of line 6 of cd fld PSdata.
More specifically, a SendHCMessage callback causes HyperCard to copy the data described by the user into a global called TD3270temp. GetGlobal is then used to fetch the data that HyperCard copied. A pseudo-code form of the process is as follows:
--4
get the users container description.
put put <users container description> into TD3270temp into msg
send msg to HyperCard using SendHCMessage
get the TD3270temp data using GetGlobal
send the TD3270temp data to the mainframe
The reverse process is used for receiving data from the mainframe. The pseudo-code is as follows:
--5
get the users container description
put the mainframe data into TD3270temp using SetGlobal
put put TD3270temp into <users container description> into msg
send msg to HyperCard using SendHCMessage
(HyperCard puts the mainframe data into the users container)
The code to get the data from the users container is as follows:
/* 6 */
void getContainerData(paramPtr, container, hGlobalData)
/*
Get the data from the container described in container.
kTmpGlobalPname is a hypercard global used for
temporary storage.
Return a handle to the HyperCard data.
The HyperCard data is stored as a C string.
Method:
Send a message to HC to put the data into a
global.
Get the contents of the global.
Requires that a global by name of globalName
already be defined.
*/
XCmdPtr paramPtr;
char *container;
Handle *hGlobalData;
{
char msg[kMaxLenLine],
globalName[kMaxLenGlobal];
Str255 globalPname;
Handle hTemp;
/*create global */
copyPstr(globalPname, kTmpGlobalPname);
copyPtoCstr(globalName, globalPname);
hTemp = putGlobal(paramPtr, globalPname, container);
/*put container into global*/
strcpy(msg, put );
strcat(msg, container);
strcat(msg, into );
strcat(msg, globalName);
SendCstrHCMsg(paramPtr, msg);
/*SendHCMsg error in get: */
CHK_CALLBACK_ERR(msg);
/*get data from global */
*hGlobalData = GetGlobal(paramPtr, globalPname);
/*GetGlobal error (copy) in get:*/
CHK_CALLBACK_ERR(globalName);
DisposHandle(hTemp);
}/*----------------getContainerData*/
The code to put the data into the users container is as follows:
/* 7 */
int putContainerData(paramPtr, container, hData)
/*
Put the hData into the container described in
container.
The HyperCard data is stored as a C string.
hData is checked for non-NIL before proceeding.
Returns TRUE if the put was done.
Method:
Create a HC global.
Copy the data to the HC global.
Send a message to HC to put the global into a container.
*/
XCmdPtr paramPtr;
char *container;
Handle hData;
{
char msg[kMaxLenLine], globalName[kMaxLenGlobal];
Str255 globalPname;
Handle hTemp;
/*make sure we have desc and handle */
if(hData==NIL || strlen(container)<=0)
return FALSE;
/*create global */
copyPstr(globalPname, kTmpGlobalPname);
copyPtoCstr(globalName, globalPname);
hTemp = putGlobal(paramPtr, globalPname, container);
/*have container desc and handle */
/*copy handle data to temp HC global*/
SetGlobal(paramPtr, globalPname, hData);
/*SetGlobal error (copy) in put: */
CHK_CALLBACK_ERR(globalName);
/*Have HC copy from global to container*/
strcpy(msg, put );
strcat(msg, globalName);
strcat(msg, into );
strcat(msg, container);
SendCstrHCMsg(paramPtr, msg);
/*SendHCMsg error in put: */
CHK_CALLBACK_ERR(msg);
DisposHandle(hTemp);
return TRUE;
}/*----------------putContainerData*/
The CHK_CALLBACK_ERR() is a macro which tests the result of the callback and generates a call to the debugger if there was an error. These kind of macros were used for conditions which may occur during development, but are highly unlikely during production. The code is as follows:
/* 8 */
#define CHK_CALLBACK_ERR(str) \
{ \
short result; \
char msg[128]; \
if((result=paramPtr->result)!=0) \
{ \
strcpy(msg, HC callback error: ); \
strcat(msg, str); \
DebugStr(ToPstr(msg)); \
} \
}
The create global code in each of the routines is intended to create a global that can
be accessed from within the XFCN without burdening the user with defining it in the HyperTalk
script. This is based on Gary Bonds book, XCMDs for HyperCard, in which he states
In HyperCard versions greater than 1.1, SetGlobal will create the global variable and
then assign the contents of the zero-terminated string to the newly created variable. (page
101) Unfortunately, it does not work that way, the user must define the global in the HyperTalk
script. If it is not defined, then HyperCard gets or puts the global name rather than its
contents. Apple Computers Macintosh Developer Technical Support Team has not acknowledged
that this is a bug.
Dynamically Allocating & Preserving Memory
The requirement for up to eight simultaneous sessions meant that the XFCN had to
allocate enough memory for all the sessions, but each session needed needed a lot of memory
- up to 10 request blocks, 8 queue blocks, 6 buffers, 5 translation tables, and 4 record
blocks. To keep memory at a minimum, the XFCN allocates memory only when needed and
deallocates when no longer needed. In addition, the memory is maintained between XFCN
calls.
Since a single handle is preserved between XFCN calls, the single handle is the key
to preserving all the remaining memory structures. A tree structure grows from the single
memory handle and points to all the needed data structures. The handle in the HyperCard
global references a master block of data, masBlk, which in turn references a session-specific
block of data, sesBlk. The tree structure grows and shrinks depending on the number of
sessions and which commands have been launched. Figure 4, Dynamic Memory Allocation,
is a schematic of the tree structure. The definitions of the structures are as follows:
Figure 4.
/* 9 */
typedef struct
{
short num_sessions;
short reserved;
char method_global_name[kMaxLenName];
Handle hSession[kMaxSessions];
} masBlk;
typedef struct
{
char session_global_name[kMaxLenName];
BYTE conn_id;
BYTE port_id;
BYTE ps_id;
BYTE flags;
char status[kMaxLenStat];
Handle hAPIvars;
Handle hReq[kMaxReq];
Handle hQueue[kMaxQ];
Handle hTable[kMaxTab];
short tableLen[kMaxTab];
Handle hBuffer[kMaxBuf];
Handle hRecord[kMaxRec];
} sesBlk;
The structures are preserved between XFCN calls because the driver accesses the structures after the XFCN is exited. Asynchronous operation allows the driver to begin operation when the command was launched from the XFCN and then continue operation at a later time. The data structures are preserved until the driver completes the command.
For example, the Get_Update command is frequently used to get information from the host mainframe. This is a good application of asynchronous operation because the update may take a long or short time depending on what the host mainframe is doing. Consequently, the Get_Update command is launched asynchronously and control is immediately returned to HyperCard while the update is processed. The TriData3270State XFCN is used to see if the command has completed.
When the Get_Update is launched, the XFCN must provide a block of memory to the driver to store driver information and receive data as it comes from the host. In addition, the container descriptions of where to store the update information in the HyperCard stack must be saved until the driver completes the update. The container descriptions are supplied in the XFCN parameter list, but cant be used until the update is complete. Therefore, they are copied from the parameter list to the queue block until needed. Since multiple containers can be updated with one Get_Update, each container description must be saved and a update record buffer must be allocated for each. Once the update is complete and the data is placed in the HyperCard containers, the queue block is cleared and the update records buffers are released. The sequence is as follows:
HyperCard XFCN
TriData3270(GET_UPDATE, ...)
Allocate memory.
Save container descriptions.
Launch Get_Update.
Return to HyperCard.
Do something else.
.
.
.
TriData3270State(GET_UPDATE, ...)
Check for completion.
Transfer data to containers.
Clear and release memory.
Return to HyperCard.
Use updated data.
The TriData3270State(GET_UPDATE, ...) command can be put in an idle handler. When the update is complete, the containers will be updated automatically without holding up any other operations.
Because buffer and record structures are allocated only when they are needed, based on the command, memory requirements expand and contract depending on the specific command and the number of containers being referenced. The net result is that memory is kept down to the bare minimum.
Debugging
I expected that debugging the XFCNs was going to be difficult. There was no symbolic debugger support for XCMD/XFCNs and just looking at the XFCN input and output was not sufficient. I needed a way to look at the data structures being used by the driver. Since the data structures are preserved between XFCN calls, I had to devise a tool to display the structure contents on demand. Fortunately, MacsBug 6.0 was available with a little-documented feature called templates.
MacsBug templates are groups of fields which describe the name, type, and count of various data types. The templates are resources of type mxwt and loaded with MacsBug at boot time. Fields types include Byte, Word, Long, strings, and variations. New field types can be defined which are combinations of other types and both pointer and handle dereferencing are supported. The template capability is a powerful tool for defining and displaying complex data structures with a single MacsBug command.
A sample Rez file supplied with MacsBug, Templates.r, gives examples of MacsBug templates and with enough trial and error, it was possible to create templates for all the data structures in this project. The templates aided greatly in display of the data structures because they labelled the structures for easy reading and dereferenced the handles so that the entire data structure tree could be displayed.
Examples of MacsBug templates for two of the data structures which were described previously are as follows:
/* 10 */
masHan,{
master handle,^masBlk, 1
},
masBlk,{
num_sessions, Word, 1,
reserved,Word,1,
method name, Text, 32,
hSession1, ^^sesBlk,1,
hSession2, ^^sesBlk,1,
hSession3, ^^sesBlk,1,
hSession4, ^^sesBlk,1,
hSession5, ^^sesBlk,1,
hSession6, ^^sesBlk,1,
hSession7, ^^sesBlk,1,
hSession8, ^^sesBlk,1
},
sesBlk,{
global_name, Text, 32,
conn_id, Byte,1,
port_id, Byte,1,
ps_id, Byte,1,
flags, Byte,1,
status,Text,12,
hAPIvars,Long,1,
hReq0-4, Long,5,
hReq5-9, Long,5,
hQueue0-3, Long, 4,
hQueue4-7, Long, 4,
hTables0-4, Long, 5,
tableLen0-4, Word, 5,
hBuffer, Long,6,
hRecord, Long,4
},
The masBlk and sesBlk structures are also shown in conventional C structures in the section on Dynamically Allocating and Preserving Memory described above. The structures for the request blocks (hReq), queues (hQueue), tables (hTables), buffers (hBuffer), and record (hRecord) were not dereferenced in the template to keep the data display down to a usable size.
The master handle is stored as a hex string in a HyperCard global. This allows the data structure to be preserved from XFCN to XFCN call. It also has an enormous side benefit - it allows the data structure to be accessed directly from the HyperCard stack. Since the master data handle is stored as a hex string, a call to MacsBug can be implemented as an XCMD passing a command to dump the hex string with the master handle template.
The hex string is stored in a HyperCard global, TD3270memory, and is also displayed in a background field. The background field has a mouseUp handler so that simply clicking on the field calls MacsBug and displays the data. If the master data handle is undefined, then the handle background field is empty and no call is made to DebugStr. The MacsBug format to dump memory with a template is
;11
; dm <handle in hex> <template name>
The dump memory command is invoked in the following mouseUp handler:
--12
--------------------------------------------------
--
-- Call DebugStr with the memory handle and template.
-- Intended for use with MacsBug. The label
-- masHan below refers to one of many MacsBug
-- templates that were designed for TriData3270.
-- It wont do you any good unless you have
-- installed the TriData3270 templates in your MacsBug.
-- MacsBug 6.0 or higher is required for this feature.
--
-- Script byJohn R. Powers, III
-- Easy Street Software
--
-- XFCNs used: none
-- XCMDs used: DebugStr
-- HANDLERS used: none
-- FUNCTIONS used: none
--
on mouseUp
global TD3270memory
if TD3270memory is not empty then
put ;dm && TD3270memory && masHan into¬
MacsBugStr
DebugStr(MacsBugStr)
else
answer¬
Do INIT_3270_API to get memory handle.
end if
end mouseUp
For those of you that have been watching closely, you may have noticed that the MacsBug template only has a single dereference for the master handle but a double dereference for the session handles. This is because the MacsBug display memory command, dm, dereferences the master handle to a pointer. The template dereferences the resulting pointer value of the master handle one more time to get the actual data. The session handles are accessed directly by MacsBug and must be dereferenced twice as one might expect.
The debugger XCMD invoked by the mouseUp handler is as follows:
/* 13 */
/* ----------------------------------------------
Program: DebugStr XCMD
Version: v1.2
History:
05/01/89 v1.0, original
05/19/89 v1.1, use NewHandle rather than
char for P-string
05/25/89 v1.2, pass HC Handle directly rather
than copy
Author:John R. Powers, III
Easy Street Software
16373 Alexander Avenue
Monte Sereno, CA 95030
(408) 395-1158
(408) 395-3308 msg.
Computer:Mac II with System v6.0.2
Compiler:MPW-C v3.0
Usage:
DebugStr(string-to-pass-to-MacsBug)
Requires HyperCard version 1.2 or better.
Checks for DebugStr trap before attempting to
call it.
If not present, doesnt make call.
Files:
DebugStr.c This source file
TriData3270 Lab HyperCard stack for testing
Full Build:
C -b DebugStr.c -mbg off -sym off
Link -w
-rt XCMD=16373
-m ENTRYPOINT
-sg DebugStr
DebugStr.c.o
{Libraries}HyperXLib.o
{Libraries}Interface.o
{CLibraries}StdCLib.o
{CLibraries}CRuntime.o
-o TriData3270 Lab
save -a
---------------------------------------------- */
#include<HyperXCmd.h>
#include<Memory.h>
#include<OSUtils.h>
#include<Types.h>
/*
some trap definitions in case we are not using MPW 3.0
*/
#ifndef _DebugStr
#define _DebugStr0xABFF
#endif
#ifndef _Unimplemented
#define _Unimplemented 0xA89F
#endif
/*
other definitions
*/
#define TRUE1
#define FALSE 0
/*
prototypes
*/
Boolean trapAvailable(short tNumber, short tType);
Boolean isImplemented(short trap);
char *ToCstr(char * str);
char *ToPstr(char * str);
/*
main program and entry point
*/
pascal void EntryPoint(paramPtr)
XCmdPtr paramPtr;/*the HyperCard connection*/
{
Handle hBugCstr;
if(!isImplemented(_DebugStr))
return;
if(paramPtr->paramCount==1)
{
/*get first param */
HLock(hBugCstr = paramPtr->params[0]);
/*convert to P-string */
DebugStr((Str255) ToPstr(*hBugCstr));
/*restore to C-string */
ToCstr(*hBugCstr);
HUnlock(hBugCstr);
}
/*no string to pass */
else
Debugger();
}/*------------------EntryPoint
*/
Boolean trapAvailable(tNumber, tType)
/*
Test to see if the trap is available.
Adapted from p.3-12 to 3-14 in Programmers
Guide to Multifinder.
APDA #KMB017
*/
short inttNumber;
short inttType;
{
return(NGetTrapAddress(tNumber, tType) !=
GetTrapAddress(_Unimplemented));
}/*end trapAvailable -------- */
Boolean isImplemented(trap)
/*
Check to see if trap is implemented.
Adapted from p.3-12 to 3-14 in Programmers
Guide to Multifinder.
APDA #KMB017
*/
short inttrap;
{
SysEnvRectheWorld;
/*machine support trap tables? */
SysEnvirons(1, &theWorld);
if(theWorld.machineType < 0)
/*ROM does not trap tables or trap*/
return(FALSE);
else
/* check for trap */
return(trapAvailable(trap, ToolTrap));
}/*end isImplemented -------- */
/*
Converts a Pascal string to a C string.
The Pascal string is overwritten in the process.
*/
char *ToCstr(str)
char *str;
{
unsigned char length, i;
length = str[0];
/* Shift string 1 byte to the left */
for (i = 0; i < length; ++i)
str[i] = str[i+1];
/* Put zero-terminator after string */
str[length] = 0;
return(str);
}
/*
Convert a C string to a Pascal string.
The C string is overwritten in the process.
*/
char *ToPstr(str)
char *str;
{
unsigned char length, i;
/* Find end of string */
for (i = 0, length = 0; str[i] != 0; ++i)
++length;
/* Shift string 1 byte to right */
while (i--)
str[i+1] = str[i];
/* Put string length in 1st byte */
str[0] = length;
return(str);
}
In addition to direct calls from the HyperCard stack using the DebugStr XCMD, calls direct to the DebugStr() trap were inserted into the code at a variety of useful places. A small collection of debugging macros execute the DebugStr in a variety of ways. To eliminate the nuisance of constantly having MacsBug interrupt the XFCNs, I wanted to turn the DebugStr feature on and off with a switch or flag. There were a variety of choices:
a compile flag using the -d option to define a name and/or value to the preprocessor which, in turn, would be used by #ifdef and #ifndef to control compilation of the macros;
a resource flag which could be accessed every time the XFCN was called; and
a flag maintained in a HyperCard global and accessed from the XFCN with GetGlobal.
Maintaining the flag in a HyperCard global proved to be the easiest way to turn the debug on and off. A check box button was created containing the following script:
--14
--------------------------------------------------
--
-- Set the flag for XFCN internal use of DebugStr.
--
-- Script byJohn R. Powers, III
-- Easy Street Software
--
-- XFCNs used: none
-- XCMDs used: none
-- HANDLERS used: none
-- FUNCTIONS used: none
--
on mouseUp
global TD3270debug
answer Set debug flag. with On or Off
if it is On then
answer Debug when? with Before or After¬
or Both
if it is Before then put 1 into TD3270debug
else if it is After then put 2 into¬
TD3270debug
else if it is Both then put 3 into¬
TD3270debug
set the hilite of target to true
else
put 0 into TD3270debug
set the hilite of target to false
end if
end mouseUp
To avoid confusion, the check box button is hidden when the master memory handle is undefined (empty).
The TriData3270 XFCN uses GetGlobal to access the global, TD3270debug, to determine whether or not DebugStr should be called internally. The code to get the debug flag is as follows:
/* 15 */
short fetchDebugFlag(paramPtr, globalPname)
/*
Fetch the debugFlag stored in globalPname
Returns the debugFlag, 0 if no global found.
*/
XCmdPtr paramPtr;/*HyperCard connection */
Str255 globalPname;
{
Handle hGlobalString;
Str255 globalPstr;
long longFlag;
HLock(hGlobalString = GetGlobal(paramPtr,
globalPname));
copyCtoPstr(globalPstr, *hGlobalString);
StringToNum(globalPstr, &longFlag);
HUnlock(hGlobalString);
return (longFlag & 0xFFFF);
}/*-------------------- fetchDebugFlag */
Macros are used within the code to determine the flag state and are as follows:
/* 16 */
#define DEBUG_AFTER(flag) \
{ \
if(flag==kDebugAfter||flag==kDebugBoth) \
DebugStr(\pAfter);\
}
#define DEBUG_BEFORE(flag)\
{ \
if(flag==kDebugBefore||flag==kDebugBoth) \
DebugStr(\pBefore); \
}
Storing flags in HyperCard globals is a powerful way to pass debugging information to and from your XCMD/XFCNs.
The Wild XFCN is Captured
In the course of this adventure, we devised a set of XCMD and XFCN tools to allow the HyperCard user to communicate with an IBM host mainframe. These tools permit up to eight simultaneous sessions with synchronous and asynchronous operation. A number of problems were overcome, allowing the preservation of data between XFCN calls, eliminating duplicate code, permitting complete chunking expressions, dynamically allocating memory, and simplifying XCMD/XFCN debugging . In addition, a stack was written to allow the user to exercise all the commands individually.
The next adventure is for the users of the TriData 3270 HyperCard Toolkit. Heres hoping their adventure will be as fun and successful as mine.
Acknowledgements
Keith Odom, Manager of Macintosh Applications for Tri-Data Systems, Inc., is the person who set the challenging goals for the software, taught me a lot about the world of the 3270, and asked a lot of intelligent questions along the way. May all my customers be so good.
Dan Shafer is appreciated for his suggestions, encouragement, and friendship.