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#
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.
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.
// <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:
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. 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.