Calling IBM iSeries RPG from C# using CWBX API CallsThis 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#

A look at how we can use the IBM CWBX library to directly call IBM RPG programs from within .Net and get the response back. We also cover a few performance improvements and an alternative method which is even faster.
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, you may need to call a program on the AS400, which may include some form of business logic rather than a direct database query. For this, we will use CWBX to call 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 must add a reference to the CWBX library from your application. This library provides information on how we will call 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 know nothing about RPG or AS400 programming; I only know that you need a program that accepts parameters in and out.
I'm going to create a console application that will call 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.
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 include 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 works, I found the performance quite slow if you use multiple calls. It takes an age to create a new AS400System object and connect, and it takes a while for the string converter to convert a string to an EBCDIC byte array.
Here is my solution to the performance issues surrounding the stringConverter class. You can use this instead of stringConverter to shave seconds off parameter string conversion times.
/// <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 convert one format to another using a lookup table. Much, much faster than whatever the IBM library is doing. To use it, replace the following:
parameters["in"].Value = stringConverter.ToBytes(param.PadRight(paramLength, ' '));
with
parameters["in"].Value = ConvertASCIItoEBCDIC(param.PadRight(paramLength, ' '))
and
result = stringConverter.FromBytes(parameters["out"].Value);
with
result = ConvertEBCDICtoASCII(parameters["out"].Value);
Other performance issues surround the use of the AS400System object. Creating a new object and connecting to the system takes around 3-5 seconds. 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.
However, we found this method not fast enough to run a busy e-commerce website. We soon switched to a slightly different method, in which we call DB2 stored procedures directly using the ODBC drivers. These procedures, in turn, are called RPG programs. This ended up being much faster.