August 19, 2008  
  You are here:  Blog

QTCommunicationSimplified50.gif


Try Radicomm's Quicktalk, the first enterprise-class Push-To-Talk application for your Motorola/Symbol rugged devices, in your distribution center for free.

Click here to enter the 21st Century.


Calling AS/400 (AS400) RPG Programs/APIs From .NET
 
Location: BlogsNetSplore's Blog    
Posted by: Joe Rattz 1/19/2007 7:00:00 PM

Since posting my article titled "Calling AS/400 (AS400) RPG Programs From ASP.NET", I have gotten a good bit of feedback and questions. This is further evidence of how difficult it is to find good information about interfacing with the AS/400. Due to some of the questions and comments posted about that article, I thought I would do a little more research, and post a follow-up article.

There is a downloadable sample project at the end of this article.

Be sure to check out part 1 in this series by clicking here.

 

The Disclaimer

Again, I provide the disclaimer that I am very ignorant about the AS/400. I come from a C/C++/C#/Java OS/2-Unix-Windows background. But, due to the lack of availability, I thought I would go ahead and humilate myself even further for your amusement and entertainment. Go ahead, laugh at my misuse of AS/400 terms!

This article is about calling programs, typically written in RPG, on the AS/400 from .NET using the cwbx.dll. While the first article refers to ASP.NET, this approach works for any .NET code. In this article, I am using the code interfacing with the AS/400 from a WinForms application.

Two questions I received about the first article that interested me were, can you call programs that return arrays, and can you call programs that return structures. The answer is yes. In fact, you can call programs that return a structure with an array of structures. And in fact, in this article I will.

When I first read the questions posed concerning returning structures and arrays, I read the online IBM help for the cwbx.dll and saw that you supposedly could even call AS/400 APIs with it. That really piqued my interest.

In this article, I will discuss the code for calling the AS/400 API to return a list of the subsystems, QWCLASBS. Because of the complicated nature of making this call, you cannot actually just call that single API because it returns the list of subsystems in a User Space, so we must also call the APIs for creating and deleting the User Space, as well as reading data from a User Space. While in the article, I will only discuss the call to QWCLASBS, I will post the code for creating, deleting, and reading the User Space. You should know though that this code is in very rough shape at this point, but I don't know when or if I will get the chance to clean it up, or abstract it better. So, there is virtually no error checking, and it is very verbose because it isn't abstracted any at all.

For this example, the code I was writing was intended to ultimately be an AS/400 API interface for many, many APIs. Because of this and the way the code was working out, I did not use the same base class I was using in the first article.

In this example, I will be calling the QWCLASBS API, and the first thing you need to know is what the specification for the API is. For your general reference, here is a link:

API Finder: http://as400bks.rochester.ibm.com/iseries/v5r2/ic2924/index.htm?info/apis/apifinder.htm

Internet Explorer has a very difficult time on that page. Firefox handles it a little better, but still took a long time to fully render. I don't know what IBM is doing, but it isn't very good. Once you are on that page, enter QWCLASBS in the "Search by name" section and click the Go button. Then click on the link for "List Active Subsystems (QWCLASBS)". You should then be looking at the specifications for the QWCLASBS API.

The QWCLASBS API Specification

Here are the required parameters:

Field Number Field Name Direction Type
1 Qualified user space name Input Char(20)
2 Format name Input Char(8)
3 Error code I/O Char(*)

And, the qualified user space name is further defined as:

Offset (Decimal) Type Field
0 CHAR(10) User space name
10 CHAR(10) User space library specified

The Parameter Field Lengths and Names

I will use an enum to store the field lengths to reference in the code:

enum QWCLASBSDataLengths : int
{
    iUserSpaceName = 10,
    iUserSpaceLibrary = 10,
    iFormatName = 8,
    iErrorCode = 8,
    oDataOffset = 4,
    oDataSectionSize = 4,
    oEntryCount = 4,
    oEntrySize = 4,
    oSubsystemName = 10,
    oSussystemLibraryName = 10,
};

For the members in that enum, if the name begins with 'i', it is an input parameter. If it begins with 'o', it is an output parameter.

Also, at the top of my code, I am going to create some strings to reference the parameters by name:

string QUALIFIED_USERSPACE_NAME = "Qualified User Space Name";
string FORMAT_NAME = "Format Name";
string ERROR_CODE = "Error Code";

string DATA_OFFSET = "Data Offset";
string DATA_SECTION_SIZE = "Section Size";
string ENTRY_COUNT = "Entry Count";
string ENTRY_SIZE = "Entry Size";

string SUBSYSTEM_NAME = "Subsystem Name";
string SUBSYSTEM_LIBRARY_NAME = "Subsystem Library Name";

CWBX Class Initialization

First, I must create all the base cwbx classes needed to call programs on the AS/400:

cwbx.AS400System as400 = new cwbx.AS400SystemClass();
as400.Define(server); // server is a string for your server name.
cwbx.Program program = new cwbx.Program();
program.system = as400;

Next, I set it up for the API I am going to call:

program.LibraryName = "QSYS";
program.ProgramName = "QWCLASBS";

Now I will create some variables for the user space name and initialize them:

string userSpaceName = "STOREFRONT";
string userSpaceLibrary = "QTEMP";

Next I will create some data type converters:

cwbx.StringConverter stringConverter = new cwbx.StringConverterClass();
cwbx.LongConverter longConverter = new cwbx.LongConverterClass();

I then call another method I wrote to create a user space:

CreateUserSpace(server, userId, password, userSpaceName, userSpaceLibrary);

Define and Initialize the Input Parameters

Create a ProgramParameters class:

cwbx.ProgramParameters parameters = new cwbx.ProgramParametersClass();

Next I create a properly configured user space name. It must be 20 bytes with the first 10 being the user space name and the second 10 being the user space library. Interestingly, I thought that I should be able to just create 2 parameters each 10 bytes long, as opposed to 1 parameter 20 bytes long, but this did not work. I got an error complaining about the number of parameters being wrong.

string qualifiedUserSpaceName =
    String.Format("{0,-10}{1,-10}", userSpaceName, userSpaceLibrary);

Create the user space parameter:

parameters.Append(QUALIFIED_USERSPACE_NAME,
    cwbx.cwbrcParameterTypeEnum.cwbrcInput,
    (int)QWCLASBSDataLengths.iUserSpaceName 
        + (int)QWCLASBSDataLengths.iUserSpaceLibrary);

stringConverter.Length =
    (int)QWCLASBSDataLengths.iUserSpaceName
        + (int)QWCLASBSDataLengths.iUserSpaceLibrary;

parameters[QUALIFIED_USERSPACE_NAME].Value = 
    stringConverter.ToBytes(qualifiedUserSpaceName);

Notice that I am using my enum anywhere a size is needed and my string for the parameter name.

Now I will create the Format Name parameter and the Error Code parameter. The API spec tells us to use SBSL0100 for the format name:

parameters.Append(FORMAT_NAME,
    cwbx.cwbrcParameterTypeEnum.cwbrcInput,
    (int)QWCLASBSDataLengths.iFormatName);

stringConverter.Length = (int)QWCLASBSDataLengths.iFormatName;
parameters[FORMAT_NAME].Value = stringConverter.ToBytes("SBSL0100");

parameters.Append(ERROR_CODE,
    cwbx.cwbrcParameterTypeEnum.cwbrcInout,
    (int)QWCLASBSDataLengths.iErrorCode);

The Program Execution

Now I need to actually call the program and pass the parameters.

try
{
    program.Call(parameters);

Retrieve the Results from the User Space

Now, QWCLASBS writes the list of subsystems to the user space that we created, so now we must retrieve the data from the user space. I will call another one of my API functions to do that.

    byte[] results = 
        RetrieveUserSpace(server, userId, password, 
            userSpaceName, userSpaceLibrary, 125, 16);

The 125 is the offset into the user space, and the 16 is the number of bytes to read. To see what the data format is of the data written to in the user space, click the "User Space Format for List APIs" link in the "Format of the Generated List" section of the API specification. Here is an image of the structure format:

ListAPIOutputStructure.jpg

The format is too complicated for me to describe here, but we are interested in the 4 integers that begin at offset x7C (125). These 4 integers tell us the offset of the array of repeating subsystems, the size of the entire repeating array, the number of entries in the array, and the size of each entry. We will use most of these values for looping through the array, while retrieving the user space.

To see what that array looks like, click the "Format of the Generated List" link in the Format Name section of the API specification, you will see the format for SBSL0100. It is a repeating array of entries that looks like:

Offset (Decimal) Type Field
0 CHAR(10) Subsystem description name
10 CHAR(10) Subsystem description library name

So basically we have an array of 20 byte strings where the first 10 bytes are the subsystem description name and the second 10 bytes are the subsystem library name.

Creating the Results Output Structure

So, our last code returned a 16 byte array containing the 4 integers we need to loop through the subsystems. Next we need to create a StructureClass that we can use to get those 4 integers out of the 16 byte array. This is the most important part for learning how to work with structures and arrays:

    cwbx.Structure mystruct = new cwbx.StructureClass();
    mystruct.Bytes = results;

Next, we create 4, 4 byte fields in that structure class:

    mystruct.Fields.Append(DATA_OFFSET, (int)QWCLASBSDataLengths.oDataOffset);
    mystruct.Fields.Append(DATA_SECTION_SIZE, 
        (int)QWCLASBSDataLengths.oDataSectionSize);
    mystruct.Fields.Append(ENTRY_COUNT, (int)QWCLASBSDataLengths.oEntryCount);
    mystruct.Fields.Append(ENTRY_SIZE, (int)QWCLASBSDataLengths.oEntrySize);

So now we have defined that Structure class to have the 4 integer fields. Now, I'll read those 4 integers:

    int offset = longConverter.FromBytes(mystruct.Fields[DATA_OFFSET].Value);
    int sectionSize = 
        longConverter.FromBytes(mystruct.Fields[DATA_SECTION_SIZE].Value);
    int entryCount = longConverter.FromBytes(mystruct.Fields[ENTRY_COUNT].Value);
    int entrySize = longConverter.FromBytes(mystruct.Fields[ENTRY_SIZE].Value);

You now see how to access data that is returned in a structure.

Retrieving the Output Structure from the User Space

So now I know where the array of subsystems is (the offset), how big that array is, how many elements are in the array, and how big each element is. So first, I am going to go retrieve the data from the user space by specifying the offset and the section size to return a single byte array with all subsystems:

    results = 
        RetrieveUserSpace(server, userId, password, userSpaceName, 
            userSpaceLibrary, offset, sectionSize);

So now the results array of bytes contains all the subsystems.

Create a Structure for the Each Array Element

Now I need to build yet another Structure class to define what each element of the array looks like:

    cwbx.Structure listItemStruct = new cwbx.StructureClass();
    listItemStruct.Fields.Append(SUBSYSTEM_NAME, 
        (int)QWCLASBSDataLengths.oSubsystemName);
    listItemStruct.Fields.Append(SUBSYSTEM_LIBRARY_NAME, 
        (int)QWCLASBSDataLengths.oSussystemLibraryName);

So that structure is now defined to have two 10 byte fields.

Next I will create a C# 2.0 generic List:

    List<SubsystemData> list = new List<SunsystemData>();

This is similar to generics in Java and templates in C++. If you are not familiar with any of these concepts, you may want to read up on C# generics. I'll summarize it as, it is C#'s way of dealing with data types in a generic way so that they do not have to be specified. This allows code to be very flexible. That line above is declaring a list that can only contain SubsystemData class objects. When I retrieve an element from that list, I don't even have to cast it. It knows it is a SubsystemData object.

Looping Through the Array of Subsystems

Now I need to loop for the number of subsystems in the array:

    for(int i = 0; i < entryCount; i++)
    {

Next I will call a function I wrote to return a portion of a byte array. Since we have this big byte array containing all of the subsystems, I want to return just a portion of that byte array to get a single element in the array:

        listItemStruct.Bytes = GetSubByteArray(results, i * entrySize, entrySize);

The second parameter of that function call is the offset into the byte array, and the third is the length to read. So the first time I call it, I will pass a 0 as the offest, and 20 as the length. The second time I call it, I will pass an offset of 20 and a length of 20, and so forth.

Now I will create a SubsystemData object to add to the list:

        SubsystemData item = new SubsystemData();

Obtaining the Subsystem Data from the Structure

Now I need to read the fields out of that sub byte array I returned:

        stringConverter.Length = (int)QWCLASBSDataLengths.oSubsystemName;
        item.subsystemName = 
            stringConverter.FromBytes(
                listItemStruct.Fields[SUBSYSTEM_NAME].Value).Trim();

        stringConverter.Length = (int)QWCLASBSDataLengths.oSussystemLibraryName;
        item.subsystemLibraryName = 
            stringConverter.FromBytes(
                listItemStruct.Fields[SUBSYSTEM_LIBRARY_NAME].Value).Trim();

And add the SubsystemData object to the list:

        list.Add(item);

    }

Next I will call my API function to delete the user space, and return the list:

    DeleteUserSpace(server, userId, password, userSpaceName, userSpaceLibrary);

    return(list);
}

I know there is a lot to this code, but the important part for this article is the way I create the StructureClass objects, append fields to the structure, and then read the data out by indexing into the structure's Fields array using the name of the parameter. Creating input structures works the same way, except you are assigning the data to the structure's fields array element for each field.

The Entire Method

Here is the entire method:

enum QWCLASBSDataLengths : int
{
    iUserSpaceName = 10,
    iUserSpaceLibrary = 10,
    iFormatName = 8,
    iErrorCode = 8,
    oDataOffset = 4,
    oDataSectionSize = 4,
    oEntryCount = 4,
    oEntrySize = 4,
    oSubsystemName = 10,
    oSussystemLibraryName = 10,
};

// QWCLASBS
public List ListActiveSubsystems(string server, string userId, string password)
{
    string QUALIFIED_USERSPACE_NAME = "Qualified User Space Name";
    string FORMAT_NAME = "Format Name";
    string ERROR_CODE = "Error Code";

    string DATA_OFFSET = "Data Offset";
    string DATA_SECTION_SIZE = "Section Size";
    string ENTRY_COUNT = "Entry Count";
    string ENTRY_SIZE = "Entry Size";

    string SUBSYSTEM_NAME = "Subsystem Name";
    string SUBSYSTEM_LIBRARY_NAME = "Subsystem Library Name";

    Connect(server);

    cwbx.Program program = GetProgram(as400);
    program.LibraryName = "QSYS";
    program.ProgramName = "QWCLASBS";

    string userSpaceName = "STOREFRONT";
    string userSpaceLibrary = "QTEMP";

    cwbx.StringConverter stringConverter = new cwbx.StringConverterClass();
    cwbx.LongConverter longConverter = new cwbx.LongConverterClass();

    // Create the userspace.
    CreateUserSpace(server, userId, password, userSpaceName, userSpaceLibrary);

    // Create the parameters.
    cwbx.ProgramParameters parameters = new cwbx.ProgramParametersClass();

    // UserSpace Name and Library.
    string qualifiedUserSpaceName = 
        String.Format("{0,-10}{1,-10}", userSpaceName, userSpaceLibrary);

    parameters.Append(QUALIFIED_USERSPACE_NAME,
        cwbx.cwbrcParameterTypeEnum.cwbrcInput,
        (int)QWCLASBSDataLengths.iUserSpaceName
            + (int)QWCLASBSDataLengths.iUserSpaceLibrary);

    stringConverter.Length = (int)QWCLASBSDataLengths.iUserSpaceName
        + (int)QWCLASBSDataLengths.iUserSpaceLibrary;

    parameters[QUALIFIED_USERSPACE_NAME].Value =
        stringConverter.ToBytes(qualifiedUserSpaceName);

    // Format Name
    parameters.Append(FORMAT_NAME,
        cwbx.cwbrcParameterTypeEnum.cwbrcInput,
        (int)QWCLASBSDataLengths.iFormatName);

    stringConverter.Length = (int)QWCLASBSDataLengths.iFormatName;
    parameters[FORMAT_NAME].Value = stringConverter.ToBytes("SBSL0100");

    // Error Code
    parameters.Append(ERROR_CODE,
        cwbx.cwbrcParameterTypeEnum.cwbrcInout,
        (int)QWCLASBSDataLengths.iErrorCode);

    try
    {
        program.Call(parameters);

        // So far so good. Let's get the header from the userspace.
        byte[] results = RetrieveUserSpace(server, userId, password,
        userSpaceName, userSpaceLibrary, 125, 16);
        cwbx.Structure mystruct = new cwbx.StructureClass();
        mystruct.Bytes = results;

        mystruct.Fields.Append(DATA_OFFSET, (int)QWCLASBSDataLengths.oDataOffset);
        mystruct.Fields.Append(DATA_SECTION_SIZE, 
            (int)QWCLASBSDataLengths.oDataSectionSize);
        mystruct.Fields.Append(ENTRY_COUNT, (int)QWCLASBSDataLengths.oEntryCount);
        mystruct.Fields.Append(ENTRY_SIZE, (int)QWCLASBSDataLengths.oEntrySize);

        int offset = longConverter.FromBytes(mystruct.Fields[DATA_OFFSET].Value);
        int sectionSize = longConverter.FromBytes(mystruct.Fields[DATA_SECTION_SIZE].Value);
        int entryCount = longConverter.FromBytes(mystruct.Fields[ENTRY_COUNT].Value);
        int entrySize = longConverter.FromBytes(mystruct.Fields[ENTRY_SIZE].Value);

        // Ok, lets get the list data.
        results = RetrieveUserSpace(server, userId, password,
        userSpaceName, userSpaceLibrary, offset, sectionSize);

        // Build a reusable structure.
        cwbx.Structure listItemStruct = new cwbx.StructureClass();
        listItemStruct.Fields.Append(SUBSYSTEM_NAME,
            (int)QWCLASBSDataLengths.oSubsystemName);

        listItemStruct.Fields.Append(SUBSYSTEM_LIBRARY_NAME,
            (int)QWCLASBSDataLengths.oSussystemLibraryName);

        // Create our list.
        List<SubsystemData> list = new List<SubsystemData>();

        // Populate the list.
        for(int i = 0; i < entryCount; i++)
        {
            listItemStruct.Bytes = GetSubByteArray(results, i * entrySize, entrySize);
            SubsystemData item = new SubsystemData();

            stringConverter.Length = (int)QWCLASBSDataLengths.oSubsystemName;
            item.subsystemName =
                stringConverter.FromBytes(listItemStruct.Fields[SUBSYSTEM_NAME].Value).Trim();

            stringConverter.Length = (int)QWCLASBSDataLengths.oSussystemLibraryName;
            item.subsystemLibraryName =
                stringConverter.FromBytes(
                    listItemStruct.Fields[SUBSYSTEM_LIBRARY_NAME].Value).Trim();

            list.Add(item);
        }

        DeleteUserSpace(server, userId, password, userSpaceName, userSpaceLibrary);

        return(list);
    }
    finally
    {
        //program.system.Disconnect(cwbx.cwbcoServiceEnum.cwbcoServiceAll);
    }
}

Download The Entire Project

Disk.gifYou may download the entire Visual Studio.Net project here. It requires the .Net Framework 2.0.

Here is a screenshot of the demo application:

AS400APIDemoScreenShot.jpg

Copyright ©2007 Joe Rattz
Permalink |  Trackback

Comments (10)   Add Comment
Re: Calling AS/400 (AS400) RPG Programs/APIs From .NET    By Jai on 5/3/2007 8:56:14 AM
Its very useful

thanks    By Mark Suykens on 6/18/2007 6:41:55 AM
I download your program as a starting point to understand the AS400.

Re: Calling AS/400 (AS400) RPG Programs/APIs From .NET    By Tom on 1/23/2008 6:37:03 PM
Joe,

Thanks for the examples. This is nearly literally the only thing on the internet about using cwbx. I'm wondering you have been able to/know how to pass in a timestamp parameter. The program I need to call expects a timestamp parameter (RPG type 'Z'???). It seems no matter what I try it does not like it.

I also did not find any type of "Converter" in the cwbx library to handle DateTime to Timestamps.

Any ideas?


Thanks,

Tom

Re: Calling AS/400 (AS400) RPG Programs/APIs From .NET    By Mike on 2/18/2008 9:42:00 AM
Thanks for the code. I'm writing a webservice that uses this as the hook in to the 400. It's a good bit faster than using the ibm provider, and wrapping the program with a stored procedure.

However, I haven't been able to find any information on connection pooling. I found the help doc for the cwbx.dll, but that only has a blurb that might remotely be a hint at pooling:


If a connection to the specified service already exists, no new connection will be established, and cwbOK will be returned. Each time this method is successfully called, the usage count for the connection to the specified service will be incremented. Each time Disconnect is called for the same service, the usage count will be decremented. When the usage count reaches zero, the actual connection is ended. Therefore, it is important that for every call to Connect there is a later paired call to Disconnect, so that the connection can be ended at the appropriate time. You can also call Disconnect and specify cwbcoServiceAll, which disconnects all existing connections to all services made through the specified AS400System object and resets all usage counts to 0.

Now, this seems to only use a single connection to the service. If I am making a hundred calls to this a minute, are they going to get backed up on the 400? Also, how do I manage the fact that connections are expensive to create, and a webservice by nature is going to be a separate call, so i'm going to be making multiple connections to the server with the same username?

Have you dealt with any of this?

Thanks,
Mike

Re: Calling AS/400 (AS400) RPG Programs/APIs From .NET    By Thomas on 4/15/2008 3:38:17 PM
Very useful article.
Using cwbx.dll, I'm wrapping AS/400 program calls into a .NET Windows Service program. Has anyone found a way to suppress cwbx.dll error dialogs that might occur during communications failures, etc.?

Re: Calling AS/400 (AS400) RPG Programs/APIs From .NET    By Tom C. on 4/21/2008 10:21:52 AM
I want to reiterate the collective "thank you" for posting this. There is very, very little about this topic on the web.

I, like the other Tom, need to pass in a timestamp parameter as well. I was wondering if anyone had found any information on how to do th is?

Thanks,

Tom C.

Re: Calling AS/400 (AS400) RPG Programs/APIs From .NET    By Jack on 4/22/2008 9:37:39 AM
Hi,

I was looking how to call RPG from .Net. Thanks for this article. But now I wonder where i can download this famous "cwbx.dll". Where does it come from ? What do i need to install on my server ? Is it free ?

Thanks for your answers !

Jack

Re: Calling AS/400 (AS400) RPG Programs/APIs From .NET    By Joe Rattz on 4/22/2008 10:35:59 AM
Jack, the cwbx.dll is included and installed with IBM iSeries Access for Windows and is in my "C:\Program Files\IBM\Client Access\Shared" directory. It is not free, but seems to be commonly available to IBM shops with AS/400s.

Re: Calling AS/400 (AS400) RPG Programs/APIs From .NET    By Joe Rattz on 4/22/2008 10:37:15 AM
Jack, also read this post:

http://www.netsplore.com/PublicPortal/blog.aspx?EntryID=24

Re: Calling AS/400 (AS400) RPG Programs/APIs From .NET    By Rick on 6/16/2008 8:55:06 PM
The best sites I have seen for learning how to go from RPG Fixed Format to RPG Free Format with a side by side comparison is:

http://rickh164.googlepages.com/RPGFREEFORMAT.htm

http://rickh164.googlepages.com/rpgtipsandtricks

http://rickh164.googlepages.com/


Your name:
Title:
Comment:
Add Comment   Cancel 
 
Module Border Module Border
My Book

 

Module Border Module Border
Blog Archive
   
    

  Home|Freebies|Blog|Services|Articles|ASP.NET Depot|DotNetNuke Central|Contact Us
  Copyright 2005 Netsplore Terms Of Use Privacy Statement