contex/P Tutorial

This tutorial demonstrates how to write a RFC client application for contex/P.

The goal

We want to develop a small program that lists all users in an R/3 system along with some useful information like last login date, whether the user is locked, number of failed logins and so on.
The information we want is contained in the table USR02. Here is a screenshot of the dictionary display from the SAP GUI:

 
 

Unfortunately, there is no function module that uses that table directly (at least not in SAP release 40B. Note that in newer SAP releases there are BAPI function modules that deal with user administration).

At this point we have two choices: 

  1. We could write our own small function module in ABAP that would read USR02 in an internal table or
  2. We could use a generic function module like RFC_GET_TABLE_ENTRIES that reads any table and get the information we need this way.
Although writing an extra function module for the given task seems like overkill, we'll show both solutions to demonstrate the principle.

Solution with own ABAP function module

Since this is not an ABAP tutorial, I'm leaving out all the glory details of how to create an ABAP function module. Here is the result:

 
FUNCTION Z_READ_USR02.
*"----------------------------------------------------------------------
*"*"Local Interface:
*"TABLES
*"USERS STRUCTURE USR02
*"----------------------------------------------------------------------
TABLES: USR02.
SELECT * INTO TABLE USERS FROM USR02 CLIENT SPECIFIED.
ENDFUNCTION.
What we have to do now is to "translate" this interface to perl so contex/P could understand it. We could start out with the function module itself. For example:
 
RFC->function("ZReadUsr02",
    {},# no import parameters
    {},# no export parameters
    { Users => ..
Wait a minute! At this point, we already need the definition of the structure of table USR02! But this table has dozens of fields. We had to write something like
my $usr02 = RFC->struct("Usr02",
              Mandt => RFC->type('C', 3),
              Bname => RFC->type('C', 12),
              ...
The probability to make a typing error this way is very high. Should we misspell some field name, our program will fail later when we try to access this field. Should we miss a field completely, wrong offsets of all the following fields will be computed. The same would be the case if we make a typing error in some length value. Such errors are very hard to trace.
So the question rises: Is there no better way that avoids this kind of errors? And, by the way, saves us from stupid typing work?

The virtue of laziness

The answer is, fortunately, yes! All information needed is already in the SAP data dictionary and there is a tool that translates this information into perl. The tool is itself written in perl and uses contex/P. Let's give it a try:
    perl meta.pl CON -c ZReadUsr02
meta.pl will make a connection to system CON (which must be defined in saprfc.ini) and create a prototype of a RFC client (-c) program that calls function module Z_READ_USR02. The output of this command looks like this:

 
Connection to CON opened.
remote integer format is BIG
SAP release is 40B
Getting function interface for Z_READ_USR02 ...
Got 1 rows.
Getting structure USR02 ...
Creating Usr02.pm
writing ZReadUsr02.pl ...
appending sub client to ZReadUsr02.pl ...
appending epilog to ZReadUsr02.pl ...
The program discovered itself that we need the structure definition of USR02 and wrote it to file Usr02.pm. Here we have what we were too lazy to type it in by hand:

 
#
#   This describes the ABAP structure USR02
#
RFC->struct("Usr02",
        "Mandt"       => RFC->type('C', 3, 0),
        "Bname"       => RFC->type('C', 12, 0),
        "Bcode"       => RFC->type('X', 8, 0),
        "Gltgv"       => RFC->type('D', 8, 0),
        "Gltgb"       => RFC->type('D', 8, 0),
        "Ustyp"       => RFC->type('C', 1, 0),
        "Class"       => RFC->type('C', 12, 0),
        "Locnt"       => RFC->type('i', 1, 0),
        "Uflag"       => RFC->type('i', 1, 0),
        "Accnt"       => RFC->type('C', 12, 0),
        "Aname"       => RFC->type('C', 12, 0),
        "Erdat"       => RFC->type('D', 8, 0),
        "Trdat"       => RFC->type('D', 8, 0),
        "Ltime"       => RFC->type('T', 6, 0),
        "Ocod1"       => RFC->type('X', 8, 0),
        "Bcda1"       => RFC->type('D', 8, 0),
        "Codv1"       => RFC->type('C', 1, 0),
        "Ocod2"       => RFC->type('X', 8, 0),
        "Bcda2"       => RFC->type('D', 8, 0),
        "Codv2"       => RFC->type('C', 1, 0),
        "Ocod3"       => RFC->type('X', 8, 0),
        "Bcda3"       => RFC->type('D', 8, 0),
        "Codv3"       => RFC->type('C', 1, 0),
        "Ocod4"       => RFC->type('X', 8, 0),
        "Bcda4"       => RFC->type('D', 8, 0),
        "Codv4"       => RFC->type('C', 1, 0),
        "Ocod5"       => RFC->type('X', 8, 0),
        "Bcda5"       => RFC->type('D', 8, 0),
        "Codv5"       => RFC->type('C', 1, 0),
        "Versn"       => RFC->type('C', 3, 0),
        "Codvn"       => RFC->type('C', 1, 0),
        "Tzone"       => RFC->type('C', 6, 0),
);

When we require this file in a perl script, the call to RFC->struct creates a new package RFC::S::Usr02 on the fly. This is from then on the contex/P type name of Usr02 structure objects. See the documentation for details.
All what's left to do is to adapt our prototype perl script to our needs. First of all, lets give it a sane name:
    mv ZreadUsr02.pl lsr3users.pl
Let's have a look at the contents of lsr3users.pl:
#!c:\programme\perl\bin\MSWin32-x86\perl.exe -w
# 
#   This file was generated by meta.pl
#
#   Look for comments starting with "#TODO:" for
#   further advice how to make this program run.
#

use strict;
use warnings;

use RFC;

The first line is pretty useless in a Windows environment, but it also does no harm. UNIX users, however, may want to "chmod +x" their script and run it by just typing it's name. We assume clean and safe programming, so "strict" and "warnings" are being used. Finally, we need to use "RFC" to get the contex/P functionality.

For your convenience, places where you have to insert or change some code are marked with "#TODO" comments.

use FindBin;
use lib ($FindBin::Bin);

These lines make perl look for "require"d files also in the directory where the script lives.
#
#       The following files are needed for your program to run.
#       They have been automatically generated by meta.pl, too.
#       If you move ZReadUsr02.pl to another place, remember to
#       move the used *.pm files to the same destination
#       or to install them into one of
#       the system-wide perl include directories.
#
require q{Usr02.pm};
Our structure definition is executed here. It would be also possible to replace the require by the contents of file Usr02.pm.
#TODO: The next 1 lines define variables for easy
#  access to your tables.
my $users = RFC->table("RFC::S::Usr02");


my $ZReadUsr02 = RFC->function("Z_READ_USR02",
        {  # parameters the function gets from caller
        },
        {  # parameters the function gives back to caller
        },
        {  # tables ...
                "USERS" =>      $users,
        });

Later we can refer to the result table with $ZreadUsr02->Users or just $users. And we have now a RFC::Function object that describes our ABAP function module.
#
#       The following subroutine implements the RFC client
#

sub client {
        my $conn = RFC->connect("CON",#TODO: choose destination
                        "000",  #TODO: enter correct client number.
                        "",     #TODO: enter user name
                        "")     #TODO: enter password
        or die "Can't connect to R/3: ".RFC::lastError;


This time we actually have to change something. We can put actual values for client, user and password or we can pass "undef" to make contex/P look in the environment or ask the user. Perhaps it"s not too insecure to hard code the client number and the user name.
If you're really going to hard code the password as well, make sure the following:
Anyway, remember that having a password in clear text in some file is still a big security hole. The safe method is to pass "undef" for the password and let contex/P prompt the user for it.
        $users->create; # refresh table


        #TODO: You might want to set parameter
        #values here, for example:
        # $ZReadUsr02->MYPARAM("foo");


Since there are neither import nor export parameters, nothing is to do here actually.
        my $rc;
        eval { $rc = $ZReadUsr02->call($conn); } or do {
                if (!$@) {              # no exception
                        warn "RFC Failure: ".RFC::lastError;
                        #TODO: handle RFC failures
                }
                else {  # system exception
                        my $le = RFC::lastError;
                        my $exc = $@;
                        $exc =~ s/\n//;
                        die <<DIE;
While trying to execute function module Z_READ_USR02,
the RFC library raised a $exc.
Usually, this happens for the following reasons:
  * The function module does not exist.
  * You gave incomplete or wrong log on information.
Consult the following message from the RFC library
to find out why the exception happened:
$le
DIE
                }
        };      # end error handling


This is the standard way to call an ABAP function module. This code catches system exceptions and detects RFC failures. We can leave that at the moment.
        #TODO: process the data you received from ZReadUsr02.

At this point, the function module has been called and, if $rc is not false, succeeded. The result table should now have some rows we can work with. Let's insert the following code here:
        return unless $rc;      # warning already printed.
        my @users = $users->getrows;
        print "Our R/3 has ", scalar @users, " users\n";
        foreach (sort { $a->Mandt cmp $b->Mandt } @users) {
                printf "%12s client %3s last login was %s %s\n",
                        $_->Bname, $_->Mandt, $_->Trdat,
                        $_->Uflag ? "LOCKED!" : "";
        };

First, the contents of the internal table is assigned to a perl array to make life easier. 
What the perl array really contains are references to RFC::S::Usr02 objects. Note how easy it is for example to sort by a specific field due to the field access via field names.
The "foreach" walks a list of objects, sorted by client and prints one line per object (user).
The rest is just a simple main program. We don't have to change it as long as we don't need fancy argument handling etc.
}        # end of sub client


#
#       This is the main program
#

unlink './dev_rfc';     #remove old trace file

client();


Note that besides setting the connection parameters all we had to do was to insert 8 lines of perl code to get a running program! The output looks like this:
Our R/3 has 23 users
        DDIC client 000 last login was 20000214 
        SAP* client 000 last login was 19990503 
     SAPCPIC client 000 last login was 19960903 
      TMSADM client 000 last login was 00000000 
       ADMIN client 000 last login was 19990925 
     SAPCPIC client 001 last login was 19960903 
        DDIC client 001 last login was 19980421 
        SAP* client 001 last login was 19980421 
        DDIC client 005 last login was 19980421 
        SAP* client 005 last login was 19990429 
     SAPCPIC client 005 last login was 19960903 
        MATT client 005 last login was 20010103 
     BRUCKER client 005 last login was 20010523 
        KOCH client 005 last login was 20010522 
     RÖDIGER client 005 last login was 00000000 LOCKED!
    WECHSUNG client 005 last login was 20010817 
   ESSENBERG client 005 last login was 20000214 
     BANNING client 005 last login was 20000526 
    WF-BATCH client 005 last login was 00000000 
     MQSDEMO client 005 last login was 00000000 
      AMTRIX client 005 last login was 20010720 
  EARLYWATCH client 066 last login was 19980414 
        SAP* client 066 last login was 19980416 

And, it runs quite fast. It's generally faster then to start a SAPGUI.

Here are the files the completed solution consists of: lsr3users.pl and Usr02.pm.

Solution using a generic function module

This tiime we solve the goal without writing any SAP code but instead use the function module RFC_GET_TABLE_ENTRIES. As far as my experience reaches, it is always better not to modify R/3 as long as there is a way around. Of course, this makes the solution slightly more complicated.

We start again by letting meta.pl write a prototype script for RFC_GET_TABLE_ENTRIES.

meta.pl CON -c RFC_GET_TABLE_ENTRIES
The results are the files RfcGetTableEnries.pl and Tab512.pm. By looking at the script, we can determine the interface of the function module:
my $entries = RFC->table("RFC::S::TAB512");
my $RfcGetTableEntries = RFC->function("RFC_GET_TABLE_ENTRIES",
        {  # parameters the function gets from caller
                # BYPASS_BUFFER is optional and defaults to SPACE
                "BYPASS_BUFFER" =>      (RFC->type('C', 1)),
                # FROM_KEY is optional and defaults to SPACE
                "FROM_KEY"      =>      (RFC->type('C', 72)),
                # GEN_KEY is optional and defaults to SPACE
                "GEN_KEY"       =>      (RFC->type('C', 72)),
                "MAX_ENTRIES"   =>      (RFC->type('I', 4)),
                "TABLE_NAME"    =>      (RFC->type('C', 30)),
                # TO_KEY is optional and defaults to SPACE
                "TO_KEY"        =>      (RFC->type('C', 72)),
        },
        {  # parameters the function gives back to caller
                "NUMBER_OF_ENTRIES"     =>      (RFC->type('I', 4)),
        },
        {  # tables ...
                "ENTRIES"       =>      $entries,
        });
This time, we have a lot of parameters. Unused optional parameters can (and should) be commented out in the name of reduced complexity, memory usage and network ressources needed. Since we want all users at this time, we can get rid of FROM_KEY, GEN_KEY and TO_KEY. Also, BYPASS_BUFFER can be left out.

The remaining parameters are the TABLE_NAME and MAX_ENTRIES. We set them like this:
 

        #TODO: You might want to set parameter values here, ...
        
        $RfcGetTableEntries->TableName("Usr02");
        $RfcGetTableEntries->MaxEntries(999999);        # not really a limit, hopefully
The tables section refers to the variable $entries declared above.  Since the purpose of the function module is  to read any table, they decided to put the contents of each row in one single character field of length 512. Since tables must contain structures, not strings or anything else, they made a structure TAB512 that contains just such a long character field.

Exceptions of ABAP function modules

When we compare the function call code that meta.pl generated for Z_READ_USR02 and RFC_GET_TABLE_ENTRIES, we find some new elsif {} code branches in the latter code:
 
    eval { $rc = $RfcGetTableEntries->call($conn); } or do {
        if (!$@) {      # no exception
            warn "RFC Failure: ".RFC::lastError;
            #TODO: handle RFC failures
        }
        elsif ($@ eq "INTERNAL_ERROR\n") {  #
            #TODO: Handle the INTERNAL_ERROR exception
            die "RFC_GET_TABLE_ENTRIES raised exception $@";
        }
        elsif ($@ eq "TABLE_EMPTY\n") { #
            #TODO: Handle the TABLE_EMPTY exception
            die "RFC_GET_TABLE_ENTRIES raised exception $@";
        }
        elsif ($@ eq "TABLE_NOT_FOUND\n") { #
            #TODO: Handle the TABLE_NOT_FOUND exception
            die "RFC_GET_TABLE_ENTRIES raised exception $@";
        }
        else {  # system exception   
            ...
        }
    }
This is the way contex/P catches so called exceptions raised by the ABAP code.  The default reaction is to  die with a message. that tells which function module raised what exception.
If it were something more complex than just a user listing, it could be a good idea not to die on empty tables.  On the contrary, an empty table is not an "exception" at all, it's just a table with 0 lines. We could remedy the situation like this
 
        elsif ($@ eq "TABLE_EMPTY\n") {  #
            #TODO: Handle the TABLE_EMPTY exception
            # now we must set the result vallues since their
            # state is unknown
            $RfcGetTableEntries->NumberOfEntries(0);
            $RfcGetTableEntries->Entries->create;
        }
 

Milestone

We have set the necessary parameters and are almost ready for a first go. At this time we just want to know, how many rows the call returns:
 
    #TODO: process the data you received from RfcGetTableEntries.
    my $f = $RfcGetTableEntries;        # tired of writing long name ...
    printf "%d users, %d table rows\n",
        $f->NumberOfEntries,
        $f->Entries->rows;
Since we don't know what to do in case NumberOfEntries is not the same as the number of rows in the table, we do not even check for that condition (never check for errors you can't handle). Instead, from now on, we'll completely ignore this redundant return value.
 
    iw@pingo:.../contexP > mv RfcGetTableEntries.pl solution2.pl
    iw@pingo:.../contexP > perl solution2.pl
    14 users, 14 table rows
 

Usr02 required!

An important thing we must do is to require Usr02.pm so that we can refer to RFC::S::Usr02 later in solution2.pl.  If we hadn't Usr02.pm already in our directory, we could ask meta.pl to make this file for us:
 
iw@pingo:.../contexP > perl meta.pl CON -t Usr02
The -t flag tells meta.pl to interpret the name as table (or structure) name. In solution.pl we have to add the line
require q{Usr02.pm};
A good place is after the line that requires Tab512.pm.
 

Dirty (but necessary) Tricks

We are now almost ready to complete the script. The only problem is that our function module gives us Tab512 rows when we want Usr02 rows. We know for sure that the data in each row is in fact the data of a Usr02 row, possibly padded to a length of 512 or truncated.
By the way: Did you notice that up to now we didn't care of how long a Usr02 row is, how long the fields are or in which order they appear in the structure (careful readers of Usr02.pm excepted!). We hadn't to, since contex/P handles those details behind the scenes. The only thing we had to know were the names of some interesting fields.
Now, matters are different. There are two possible cases: Guess if we can get our script to work and after that still don't know the exact length of Usr02? Look at this:
 
    if (RFC::S::Usr02->bytes > RFC::S::Tab512->bytes) {
        die "Sorry, Tab512 is too short.";
    }
    my @users = map {
            RFC::S::Usr02->pval(
                substr($_->aval, 0, RFC::S::Usr02->bytes)
            )
        } $f->Entries->getrows;
The rest is exactly the same as in lsr3users.pl:
    foreach (sort { $a->Mandt cmp $b->Mandt } @users) {
        printf "%12s client %3s last login was %s %s\n",
            $_->Bname, $_->Mandt, $_->Trdat,
            $_->Uflag ? "LOCKED!" : "";
    };
We added 3 lines of code to check for the condition that prevents us from creating the listing. (Ok, we could have written that in one line using  die ... if ...)
The trick is done in the now enhanced copy of the table rows to the perl array @users. This time we use the powerful perl map operator to map (sic!) Tab512 rows to Usr02 rows. I have indented it a bit to enhance readability, but in principle it's still one line. Here's the explanation: At the bottom the expression $f->Entries->getrows returns an array of references to RFC::S::Tab512 objects. The elements of this array are passed one after another to the map operator, which builds a new array from the results of the code inside the braces. This result array is then copied to @users.
Now the elements that the map code works with ($_) are references to RFC::S::Tab512 objects. Thus, the expression $_->aval returns the whole structure as one long string that contains our data from the current element. The initial substring in the length of the Usr02 structure is then passed to RFC::S::Usr02->pval. This treats the passed string as ABAP value of a USR02 structure and converts it to a perl value, namely, a reference to an RFC::S::Usr02 object that happens to contain this data. That's exactly what we wanted and the rest of the code that works already with such references remains unchanged.

That's it. Here is the completed script, solution2.pl.

By the way, are you still curious how long a Usr02 record really is? Let's see

iw@pingo:.../contexP > perl -MRFC -MUsr02 -e "print RFC::S::Usr02->bytes, qq{\\n}";
195
That's today in our unpatched 40B release. But who knows? Somebody may upgrade your R/3 system to 67D tomorrow. Most probably, the length of USR02 will change. You'll  notice that all your scripts where you wrote 195 literally stop working or give strange results. Now you go through all scripts and replace 195 by the new value, accidently also changing a place where 195 was not the length of Usr02 but some other important constant ...  a nightmare!
With the above code, you just re-create Usr02.pm with meta.pl. That makes all your scripts work again ...