Appeler une procédure de programme de service IBM i (AS/400) en C# (.NET) avec NTi

Introduction

Ce tutoriel montre comment appeler une procédure exportée d’un programme de service IBM i (AS/400) depuis une application C# (.NET), avec les méthodes CallServiceProgram et CallServiceProgramAsync de NTi Data Provider (disponibles à partir de NTi 5.0.0).

CallProgram couvre les programmes (*PGM). Or une grande partie de la logique ILE moderne, et de nombreuses API du système, sont livrées sous forme de programmes de service (*SRVPGM) : des bibliothèques de procédures qu’un CALL classique ne sait pas atteindre. CallServiceProgram comble cet écart : vous nommez le programme de service, l’export et les paramètres, NTi fait le reste.

Au programme :

  • ce qu’est un programme de service et un export ILE, vu depuis .NET
  • l’API système QZRUCLSP sur laquelle repose l’appel
  • deux exemples complets validés sur serveur : gethostname puis putenv
  • les formats de paramètre (ByReference, ByValue) et de retour (None, Integer, IntegerAndErrno)
  • la casse du nom d’export, les limites et la gestion d’erreur

Programme de service et export ILE, pour un développeur .NET

Un programme de service (objet *SRVPGM) est l’équivalent IBM i d’une bibliothèque partagée : une DLL .NET ou un .so Linux. Il ne se lance pas ; il expose des procédures (des fonctions ILE écrites en RPG, C, COBOL ou CL) que d’autres programmes lient et appellent. La liste des procédures visibles de l’extérieur est la table des exports, consultable avec la commande CL DSPSRVPGM SRVPGM(MALIB/MONSRVPGM) DETAIL(*PROCEXP).

Chaque export a un nom, et c’est ce nom, dans sa casse exacte, que NTi transmet au serveur. Contrairement à un programme, une procédure a une vraie signature : elle reçoit ses arguments par valeur ou par référence, et peut retourner une valeur. CallServiceProgram reflète ces trois notions : formats de paramètre, format de retour, nom d’export.


L’API QZRUCLSP sous le capot

Côté serveur, NTi s’appuie sur l’API système Call Service Program Procedure (QZRUCLSP) : elle résout le programme de service, cherche le nom dans la table des exports, effectue l’appel lié et rend la valeur de retour. NTi compose l’appel pour vous (nom d’export encodé et null-terminé, tableau des formats, réceptacle de retour) : sur le réseau, un CallServiceProgram est un appel de programme ordinaire vers QSYS/QZRUCLSP.

💡 Comme tout appel de programme, il s’exécute dans le job serveur de commandes de la connexion (QZRCSRVS), distinct du job SQL (QZDASOINIT) : QTEMP, CURLIB et variables d’environnement sont ceux de ce job. Les propriétés conn.DatabaseJob et conn.CommandJob identifient les deux jobs.


La méthode CallServiceProgram

NTiProgramParameter? CallServiceProgram(
    string library, string serviceProgram, string procedureName,
    IList parameters,
    NTiServiceProgramReturnFormat returnFormat = NTiServiceProgramReturnFormat.None,
    int procedureNameCcsid = 37);

CallServiceProgramAsync a la même signature, plus un CancellationToken final, et emprunte la même voie asynchrone réelle que CallProgramAsync.

  • library / serviceProgram : bibliothèque et programme de service (1 à 10 caractères, pliés en majuscules ; les noms entre guillemets ne sont pas supportés)
  • procedureName : nom de l’export, sensible à la casse (voir plus bas)
  • parameters : jusqu’à 7 NTiProgramParameter
  • returnFormat : None (défaut), Integer ou IntegerAndErrno
  • procedureNameCcsid : CCSID mono-octet d’encodage du nom d’export, 37 par défaut

La méthode rend un NTiProgramParameter réceptacle portant la valeur de retour de la procédure, ou null quand returnFormat vaut None.


Formats de paramètre : ByReference et ByValue

Chaque NTiProgramParameter porte une propriété ServiceProgramParameterFormat (ignorée par CallProgram) :

Format Sémantique Contrainte
ByReference (défaut) la procédure reçoit l’adresse du stockage du paramètre : elle peut le lire et y écrire aucune : chaînes, structures, buffers de toute taille
ByValue l’argument est un entier BINARY(4) passé par valeur les données doivent faire exactement 4 octets ; jamais en sortie seule

En pratique : un int déclaré par valeur dans le prototype C ou RPG (une longueur, un descripteur, des drapeaux) se passe en ByValue via new NTiProgramParameter(n), dont le constructeur produit exactement un BINARY(4). Tout le reste (chaînes, structures, buffers que la procédure remplit) reste en ByReference.


Formats de retour

NTiServiceProgramReturnFormat La procédure retourne Lecture
None rien (void) l’appel rend null
Integer un entier 4 octets rc.GetInt()
IntegerAndErrno un entier 4 octets, accompagné de la valeur d’errno rc.GetInt(), puis rc.GetInt(4) pour errno

IntegerAndErrno est le format naturel des API de type UNIX du système, qui signalent l’échec par un retour -1 et détaillent la cause dans errno.


Exemple 1 : gethostname (QSYS/QSOSRV1)

La procédure gethostname(), exportée par le programme de service QSOSRV1 de QSYS, illustre les formats mixtes. Son prototype C :

int gethostname(char *name, socklen_t namelen);

Soit : un buffer fourni par l’appelant, passé par référence, que la procédure remplit avec le nom d’hôte (chaîne C null-terminée) ; la longueur du buffer, passée par valeur ; un entier de retour (0 = succès).

using System;
using System.Collections.Generic;
using System.Data;
using Aumerial.Data.Nti;

await using var conn = new NTiConnection("server=serverName;user=userName;password=password");
await conn.OpenAsync();

// char *name : buffer de 64 octets rempli par la procédure (ByReference, le défaut)
var buffer = new NTiProgramParameter(new byte[64], ParameterDirection.InputOutput);

// socklen_t namelen : entier passé PAR VALEUR (BINARY(4), exactement 4 octets)
var length = new NTiProgramParameter(64, ParameterDirection.Input)
{
    ServiceProgramParameterFormat = NTiServiceProgramParameterFormat.ByValue
};

var rc = await conn.CallServiceProgramAsync("QSYS", "QSOSRV1", "gethostname",
    new List { buffer, length },
    NTiServiceProgramReturnFormat.Integer);

if (rc is null || rc.GetInt() != 0)
    throw new InvalidOperationException("gethostname a échoué");

// Le buffer contient une chaîne C null-terminée : on lit jusqu'au premier 0x00
byte[] data = buffer.GetBytes();
int end = Array.IndexOf(data, (byte)0);
string host = buffer.GetString(0, end < 0 ? data.Length : end);
Console.WriteLine($"Nom d'hôte : {host}");

En synchrone, même signature sans le jeton : var rc = conn.CallServiceProgram("QSYS", "QSOSRV1", "gethostname", ...);.


Exemple 2 : putenv (QSYS/QP0ZCPA)

La procédure putenv(), exportée par QP0ZCPA de QSYS, définit une variable d’environnement du job. Prototype C :

int putenv(const char *string);

Elle attend une chaîne C "VAR=valeur" null-terminée, passée par référence. La chaîne C se compose naturellement avec Append : le texte encodé en EBCDIC, suivi d’un octet 0x00 :

using System;
using System.Collections.Generic;
using System.Data;
using Aumerial.Data.Nti;

await using var conn = new NTiConnection("server=serverName;user=userName;password=password");
await conn.OpenAsync();

// Chaîne C null-terminée : texte EBCDIC (CCSID 37), puis un octet 0x00
const string assignment = "NTIVAL=DEMO";
var value = new NTiProgramParameter(assignment, assignment.Length, 37, ParameterDirection.Input)
    .Append(new byte[] { 0x00 });

var rc = await conn.CallServiceProgramAsync("QSYS", "QP0ZCPA", "putenv",
    new List { value },
    NTiServiceProgramReturnFormat.IntegerAndErrno);

if (rc is null || rc.GetInt() != 0)
    Console.WriteLine($"putenv a échoué : retour {rc?.GetInt()}, errno {rc?.GetInt(4)}");
else
    Console.WriteLine("Variable définie dans le job de commandes");

💡 La variable est définie dans le job de commandes (QZRCSRVS) : un programme RPG appelé ensuite sur la même connexion la lit avec getenv(). Le job SQL, lui, ne la voit pas : les deux jobs d’une connexion ont chacun leur environnement, leur QTEMP et leur CURLIB.


Nom d’export : la casse compte

Contrairement aux noms d’objets (bibliothèque, programme de service), pliés en majuscules, le nom d’export est transmis tel quel et comparé octet par octet à la table des exports : gethostname et GETHOSTNAME sont deux exports différents. C’est le contrat de QZRUCLSP, pas une convention NTi.

Le nom est encodé en CCSID 37 par défaut. Si la table des exports a été générée dans une autre page de code (noms contenant #, @, $ ou des caractères nationaux), passez le bon CCSID mono-octet via procedureNameCcsid ; un CCSID multi-octets est refusé avec une erreur explicite.


Limites

  • 7 paramètres maximum. Au-delà, QZRUCLSP bascule dans une convention d’appel différente (tout par pointeur) que NTi ne couvre pas : l’appel est refusé avec une erreur claire plutôt que de composer une trame hasardeuse.
  • Pas de valeur de retour pointeur. Un pointeur d’espace serveur n’a aucun sens côté client : il référence la mémoire d’un job sur l’IBM i, pas celle de votre processus .NET. Les procédures qui « retournent » du texte s’utilisent avec le pattern du buffer fourni : l’appelant passe un buffer ByReference que la procédure remplit, comme gethostname ci-dessus.
  • ByValue = 4 octets exactement. Seuls les entiers BINARY(4) passent par valeur ; toute autre taille est refusée avant l’appel.

Gestion des erreurs

Un échec (programme de service introuvable, export absent de la table, erreur levée par la procédure) remonte en NTiCommandException, avec la pile de messages du serveur dans Messages. L’export introuvable, par exemple, produit le message CPF226E :

using System;
using System.Collections.Generic;
using Aumerial.Data.Nti;

await using var conn = new NTiConnection("server=serverName;user=userName;password=password");
await conn.OpenAsync();

try
{
    // Mauvaise casse volontaire : l'export s'appelle "putenv"
    await conn.CallServiceProgramAsync("QSYS", "QP0ZCPA", "putEnv",
        new List());
}
catch (NTiCommandException ex)
{
    // CPF226E : l'export "putEnv" n'existe pas dans QP0ZCPA
    foreach (var message in ex.Messages)
        Console.WriteLine($"{message.Id} (sévérité {message.Severity}) : {message.Text}");
}

Récapitulatif

Code complet pour appeler une procédure exportée d’un programme de service IBM i depuis .NET avec NTi :

using System;
using System.Collections.Generic;
using System.Data;
using Aumerial.Data.Nti;

await using var conn = new NTiConnection("server=serverName;user=userName;password=password");
await conn.OpenAsync();

// gethostname : buffer ByReference + longueur ByValue, retour Integer
var buffer = new NTiProgramParameter(new byte[64], ParameterDirection.InputOutput);
var length = new NTiProgramParameter(64, ParameterDirection.Input)
{
    ServiceProgramParameterFormat = NTiServiceProgramParameterFormat.ByValue
};

try
{
    var rc = await conn.CallServiceProgramAsync("QSYS", "QSOSRV1", "gethostname",
        new List { buffer, length },
        NTiServiceProgramReturnFormat.Integer);

    if (rc is null || rc.GetInt() != 0)
        throw new InvalidOperationException("gethostname a échoué");

    byte[] data = buffer.GetBytes();
    int end = Array.IndexOf(data, (byte)0);
    Console.WriteLine($"Nom d'hôte : {buffer.GetString(0, end < 0 ? data.Length : end)}");
}
catch (NTiCommandException ex)
{
    foreach (var message in ex.Messages)
        Console.WriteLine($"{message.Id} (sévérité {message.Severity}) : {message.Text}");
}

Et maintenant ?

Reconnexion au serveur...

La connexion au serveur a été perdue. La page va se recharger.