Calling IBM iSeries RPG from C# using CWBX API Calls

This tutorial will show you how to use the IBM supplied CWBX library to communicate with an iSeries using API calls for calling RPG from C#

By Tim Trott | C# ASP.Net MVC | May 24, 2010
1,106 words, estimated reading time 4 minutes.

In a previous tutorial, we looked at how we can query an IBM iSeries (AS400) DB2 database directly using C# and ADO.Net. In some circumstances though, you may need to call a program on the AS400 which may include some form of business logic rather than a direct database query, and for this, we are going to use CWBX for calling RPG from C#.

The first thing you will need to do is make sure you have the latest version of IBM Client Access  (V5R3 or later) installed and make sure that you install the optional programmer's toolkit.

Next, you will need to add a reference to the CWBX library from your application. This library provides the means by which we are going to be calling RPG from C#. You can add a reference to this file from the project menu and select "Add Reference". You need to browse to "C:\Program Files\IBM Client Access\Shared\cwbx.dll". This will give you access to the IBM API's within the cwbx namespace. You can add cwbx to you using statements if you wish.

I have to say now that I know absolutely nothing about RPG or AS400 programming, all I know is you need a program that accepts parameters in and out.

I'm going to create a console application that is going to be calling RPG from C# and return a value to the user. This example is very basic and does not perform much in the way of error handling.

C#
using System;
using System.Collections.Generic;
using System.Text;
using cwbx;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            string result = string.Empty;

            StringConverter stringConverter = new StringConverterClass();

            / Define an AS400 system and connect to it
            AS400System system = new AS400System();
            system.Define("AS400");
            system.UserID = "USERNAME";
            system.Password = "PASSWORD";
            system.IPAddress = "127.0.0.1";
            system.Connect(cwbcoServiceEnum.cwbcoServiceRemoteCmd);

            / Check the connection
            if (system.IsConnected(cwbcoServiceEnum.cwbcoServiceRemoteCmd) == 1)
            {
                / Create a program object and link to a system                
                cwbx.Program program = new cwbx.Program();
                program.LibraryName = "LIBRARY";
                program.ProgramName = "RPGPROG";
                program.system = system;

                / Sample parameter data
                string param = "Example"; 
                int paramLength = 15; / Will be defined in the RGP program, so check with the programmer.

                / Create a collection of parameters associated with the program
                ProgramParameters parameters = new ProgramParameters();
                parameters.Append("in", cwbrcParameterTypeEnum.cwbrcInout, paramLength);
                parameters.Append("out", cwbrcParameterTypeEnum.cwbrcInout, paramLength);
                parameters["in"].Value = stringConverter.ToBytes(param.PadRight(paramLength, ' '));

                / Finally call the program 
                try
                {
                    program.Call(parameters);
                }
                catch (Exception ex)
                {
                    if (system.Errors.Count > 0)
                    {
                        foreach (cwbx.Error error in as400.Errors)
                        {
                            Console.WriteLine(error.Text);
                        }
                    }

                    if (program.Errors.Count > 0)
                    {
                        foreach (cwbx.Error error in program.Errors)
                        {
                            Console.WriteLine(error.Text);
                        }
                    }
                }

                result = stringConverter.FromBytes(parameters["out"].Value);
            }

            system.Disconnect(cwbcoServiceEnum.cwbcoServiceAll);
            Console.WriteLine(result);
            Console.ReadKey();
        }
    }
}

If a program falls over on the AS400 for whatever reason, the exception message contained within Ex.Message will contain the AS400 error code and message, so you can also use that to trap errors. For example, ex.Message may contain "CPA3138 - Member BLAH file BLAHBLAH at maximum size." which you can handle.

While this method does work, I found the performance very slow if you are using multiple calls. It takes an age to create a new AS400System object and connect, and it takes a while for the stringConverter to convert a string to a EBCDIC byte array.

Here is my solution to the performance issues surrounding the stringConverter class. You can use this in place of stringConverter to shave seconds off parameter string conversion times.

C#
// <summary>
// Convert an IBM EBCDIC string into ASCII
// </summary>
// <param name="strEBCDICString">IBM AS400 EBCDIC string</param>
// <returns>ASCII String</returns>
public static string ConvertEBCDICtoASCII(byte[] strEBCDICString)
{
    StringBuilder sb = new StringBuilder();
    char newc = '';

    strEBCDICString = TrimByteArray(strEBCDICString);

    for (int i = 0; i < strEBCDICString.Length; i++)
    {
        if (strEBCDICString[i] != '')
        {
            newc = Convert.ToChar(e2a[(int)strEBCDICString[i]]);
            sb.Append(newc);
        }
    }
    string result = sb.ToString();
    sb = null;

    return result;
}

// <summary>
// Convert an ASCII string to IBM EBCDIC
// </summary>
// <param name="strASCIIString">The ASCII string to convert</param>
// <returns>IBM EBCDIC array</returns>
public static byte[] ConvertASCIItoEBCDIC(string strASCIIString)
{
    UTF8Encoding encoding = new UTF8Encoding();
    byte[] result = encoding.GetBytes(strASCIIString);

    for (int i = 0; i < result.Length; i++)
    {
        result[i] = a2e[(int)result[i]];
    }

    return result;
}

// <summary>
// Trim empty or null values from a byte array
// </summary>
// <param name="input"></param>
// <returns></returns>
private static byte[] TrimByteArray(byte[] input)
{
  int i = input.Length - 1;
  while (input[i] == 0)
    --i;

  byte[] trimmed = new byte[i + 1];
  Array.Copy(input, trimmed, i + 1);
  
  return trimmed;
}

// <summary>
// Character lookup for EBCDIC ASCII Conversion
// </summary>
private static int[] e2a = new int[256]{
0, 1, 2, 3,156, 9,134,127,151,141,142, 11, 12, 13, 14, 15,
16, 17, 18, 19,157,133, 8,135, 24, 25,146,143, 28, 29, 30, 31,
128,129,130,131,132, 10, 23, 27,136,137,138,139,140, 5, 6, 7,
144,145, 22,147,148,149,150, 4,152,153,154,155, 20, 21,158, 26,
32,160,161,162,163,164,165,166,167,168, 91, 46, 60, 40, 43, 33,
38,169,170,171,172,173,174,175,176,177, 93, 36, 42, 41, 59, 94,
45, 47,178,179,180,181,182,183,184,185,124, 44, 37, 95, 62, 63,
186,187,188,189,190,191,192,193,194, 96, 58, 35, 64, 39, 61, 34,
195, 97, 98, 99,100,101,102,103,104,105,196,197,198,199,200,201,
202,106,107,108,109,110,111,112,113,114,203,204,205,206,207,208,
209,126,115,116,117,118,119,120,121,122,210,211,212,213,214,215,
216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,
123, 65, 66, 67, 68, 69, 70, 71, 72, 73,232,233,234,235,236,237,
125, 74, 75, 76, 77, 78, 79, 80, 81, 82,238,239,240,241,242,243,
92,159, 83, 84, 85, 86, 87, 88, 89, 90,244,245,246,247,248,249,
48, 49, 50, 51, 52, 53, 54, 55, 56, 57,250,251,252,253,254,255
};

// <summary>
// Character lookup for EBCDIC ASCII Conversion
// </summary>
private static byte[] a2e = new byte[256]{
0, 1, 2, 3, 55, 45, 46, 47, 22, 5, 37, 11, 12, 13, 14, 15,
16, 17, 18, 19, 60, 61, 50, 38, 24, 25, 63, 39, 28, 29, 30, 31,
64, 79,127,123, 91,108, 80,125, 77, 93, 92, 78,107, 96, 75, 97,
240,241,242,243,244,245,246,247,248,249,122, 94, 76,126,110,111,
124,193,194,195,196,197,198,199,200,201,209,210,211,212,213,214,
215,216,217,226,227,228,229,230,231,232,233, 74,224, 90, 95,109,
121,129,130,131,132,133,134,135,136,137,145,146,147,148,149,150,
151,152,153,162,163,164,165,166,167,168,169,192,106,208,161, 7,
32, 33, 34, 35, 36, 21, 6, 23, 40, 41, 42, 43, 44, 9, 10, 27,
48, 49, 26, 51, 52, 53, 54, 8, 56, 57, 58, 59, 4, 20, 62,225,
65, 66, 67, 68, 69, 70, 71, 72, 73, 81, 82, 83, 84, 85, 86, 87,
88, 89, 98, 99,100,101,102,103,104,105,112,113,114,115,116,117,
118,119,120,128,138,139,140,141,142,143,144,154,155,156,157,158,
159,160,170,171,172,173,174,175,176,177,178,179,180,181,182,183,
184,185,186,187,188,189,190,191,202,203,204,205,206,207,218,219,
220,221,222,223,234,235,236,237,238,239,250,251,252,253,254,255
};

All this does is simply convert one format to another using a lookup table. Much, much faster than whatever the IBM library is doing. To use it simply replace:

C#
parameters["in"].Value = stringConverter.ToBytes(param.PadRight(paramLength, ' '));

with

C#
parameters["in"].Value = ConvertASCIItoEBCDIC(param.PadRight(paramLength, ' '))

and

C#
result = stringConverter.FromBytes(parameters["out"].Value);

with

C#
result = ConvertEBCDICtoASCII(parameters["out"].Value);

Other performance issues surround the use of the AS400System object. It seems to take around 3-5 seconds to create a new object and connect to the system. There are a few ways around this, but the one I prefer is to use an object factory class or singleton class to dispense connections. If you are working on Windows Forms Applications then you can get away with creating a "global" variable to hold the AS400System, while on ASP.Net you can use a pool of objects and a singleton dispenser.

Overall however we found this method to be not fast enough to run a busy e-commerce website and we soon switched over to a slightly different method in which we call DB2 stored procedures directly using the ODBC drivers, and these procedures in turn call RPG programs. This ended up being much faster.

Was this article helpful to you?
 

Related ArticlesThese articles may also be of interest to you

CommentsShare your thoughts in the comments below

If you enjoyed reading this article, or it helped you in some way, all I ask in return is you leave a comment below or share this page with your friends. Thank you.

This post has 3 comment(s). Why not join the discussion!

We respect your privacy, and will not make your email public. Learn how your comment data is processed.

  1. FR

    On Friday 5th of May 2023, Francesco said

    Hi Tim, thanks for sharing this code, it has been very useful!
    Maybe you can help me more, I am trying to use cwbx from a console application or from a service application, but it seems to be very slow.

    I have a program which requires a complex input with:
    - 3 single structures (cwbx.structure)
    - 5 structure arrays. The 5 arrays contains, respectively 30, 300, 10, 20 and 10 elements with different structures.

    The total number of structures to pass is 373 plus the 5 fake structures that identifies the 5 arrays.

    If I call this program from a WPF application, it runs smoothly in 0.4230891 seconds.
    If I run the same program in a console application, in runs much more slowly in 2.5116918 seconds.

    I then added some stopwatches to calculate the timings and it resulted that program.Call(parameters) takes the same amount of time.

    The difference is in the construction and assignment of the input parameters.

    Do you know if cwbx requires a Window Handle or something like that to explain the slowdown running in console application?

  2. GR

    On Friday 28th of June 2019, Grégoire said

    Hi,

    Very useful article. Do you know if the call "program.Call(parameters)" is synchronous or not ?

    Thank you in advance,

    BR,

  3. HE

    On Thursday 6th of February 2014, Henrik said

    Could you send me the TrimByteArray method that you use? Also, do you know the difference between running command and program in AS400?