QUSLOBJ IBM i: System-API-Aufruf in .NET/C# mit NTi

Entdecken Sie, wie Sie die IBM i System-API QUSLOBJ in .NET mit NTi nutzen können, um dynamisch Objekte einer Bibliothek (FILE, PGM usw.) aus einer C#-Anwendung aufzulisten.

Illustrationsbild zu Artikel

IBM i stellt zahlreiche System-APIs bereit, die den Zugriff auf Betriebssystemressourcen ermöglichen. Einige dieser APIs liefern direkt verwertbare Daten in Form von Strukturen oder Streams, während andere die Nutzung eines User Space erfordern, um Ergebnisse zwischenzuspeichern, bevor sie verarbeitet werden können.

Die API QUSLOBJ (List Objects) gehört zur zweiten Kategorie: Sie gibt die Objektliste nicht direkt als Ausgabeparameter zurück, sondern schreibt die Ergebnisse in einen User Space, einen temporären Speicherbereich. Dieses Verfahren ist typisch für IBM i APIs, die mit dynamischen Datenmengen arbeiten, da es die Verarbeitung großer Informationsvolumen ohne feste Größenbegrenzung ermöglicht.

Ziel dieses Tutorials ist es, zu zeigen, wie man mit NTi korrekt eine Methode in .NET implementiert, um diese API aufzurufen – und wie man die IBM-Dokumentation richtig interpretiert, um die Eingabestrukturen korrekt zu erstellen und die Rückgabedaten auszuwerten.

Der vollständige Quellcode der Implementierung ist am Ende dieses Tutorials verfügbar.
Sie können Schritt für Schritt folgen, um das Verfahren zu verstehen, und anschließend zur Gesamtübersicht und direkten Nutzung zurückkehren.

Schritt 1 - Auswahl einer API: QSYS.QUSLOBJ

Um die API QUSLOBJ zu verwenden, ist der erste Schritt, ihre Dokumentation zu lesen. Die wichtigsten Schritte sind:

  • Einen User Space erstellen, um die Ergebnisse über die API QUSCRTUS zu speichern
  • Die API QUSLOBJ aufrufen und die erwarteten Eingabeparameter angeben
  • Die im User Space gespeicherten Ergebnisse mit der API QUSRTVUS lesen
  • Die zurückgegebenen Objekte durchgehen und in .NET-Objekte abbilden

Schritt 2 - Lesen der IBM-Dokumentation

Bevor Sie den Aufruf von QUSLOBJ implementieren, ist es wichtig zu verstehen, welche Parameter diese API erwartet und was sie als Ausgabe zurückliefert.

###📥Eingabeparameter:

Parameter Typ Richtung Beschreibung
User Space Char(20) Input Name (10 Zeichen) und Bibliothek (10 Zeichen), in der die Liste gespeichert wird
Format Name Char(8) Input Datenformat (in unserem Beispiel: OBJL0400)
Object & LibraryName Char(20) Input Objektname (10 Zeichen) und Bibliothek (10 Zeichen)
Object Type Char(10) Input Objekttyp (*FILE, *PGM, usw.)
Error Code Char(* ) Input / Output Fehlerstruktur (optional)

###📥Ausgabeparameter:

Die QUSLOBJ-API gibt die Objektliste nicht direkt über Ausgabeparameter zurück. Stattdessen speichert sie die Ergebnisse in einem User Space, einem temporären Speicherbereich, der als Eingabe angegeben werden muss.

Ein User Space ist ein Systemobjekt, das zur Speicherung großer Datenmengen verwendet wird – insbesondere der Ergebnisse bestimmter IBM i APIs.

Er wird vor dem Aufruf von QUSLOBJ mithilfe der API QUSCRTUS erstellt. Sobald QUSLOBJ ausgeführt wird, werden die aufgelisteten Objekte in diesem User Space geschrieben. Um die Ergebnisse abzurufen, wird die API QUSRTVUS verwendet, die den Inhalt des User Space ausliest.

Wir gehen hier nicht auf die Implementierungsdetails ein – aber wichtig ist: Alle zurückgegebenen Daten werden im User Space gespeichert und müssen gemäß der von IBM definierten Struktur analysiert werden.

Schritt 3 - Implementierung der Aufrufmethode für QUSLOBJ mit NTi

Jetzt wissen wir, wie QUSLOBJ funktioniert und wie die im User Space gespeicherten Ergebnisse interpretiert werden. Gehen wir nun zur Implementierung der Methode in C# mit NTi über.

1️⃣ Definition des Datenmodells

Zuerst müssen wir ein Datenmodell definieren, das jedes von der API zurückgegebene Objekt repräsentiert. Wir verwenden das Format OBJL0400, um detaillierte Informationen über die Objekte abzurufen. Dazu erstellen wir eine C#-Klasse, die dieser Struktur entspricht:

public class ListObjectsInformation
{
    public string? ObjectName { get; set; }
    public string? LibraryName { get; set; }
    public string? ObjectType { get; set; }
    public string? InformationStatus { get; set; }
    public string? ExtendedAttribute { get; set; }
    public string? TextDescription { get; set; }
    public int AspNumber { get; set; }
    public string? Owner { get; set; }
    public DateTime CreationDateTime { get; set; }
    public DateTime ChangeDateTime { get; set; }
    ...
}

2️⃣ Definition der Hauptmethode

Wir definieren die Methode, die wir erstellen wollen, um eine Liste von Objekten aus einer bestimmten Bibliothek abzurufen. Die API erwartet dabei die Parameter libraryName, objectName und objectType.

public static List<ListObjectsInformation> RetrieveObjectList(
    string libraryName,
    string objectName = "*ALL",
    string objectType = "*ALL"
)
{
}

3️⃣ Erstellen des User Space

Bevor QUSLOBJ aufgerufen wird, muss ein User Space in QTEMP erstellt werden – wie zuvor erklärt – um die generierte Objektliste aufnehmen zu können. Die API QUSCRTUS wird verwendet, um diesen User Space zu erstellen. Sie benötigt mehrere Parameter, darunter:

  • Den Namen des User Space, hier NTILOBJ in QTEMP
  • Eine anfängliche Größe, die dynamisch basierend auf den zurückgegebenen Daten angepasst wird
  • Einen Initialisierungswert, hier 0x00, was einem leeren Speicherbereich entspricht
  • Ein generisches Attribut und eine Beschreibung

Da die Größe der zurückgegebenen Daten im Voraus nicht bekannt ist, wird eine while-Schleife verwendet, um zu prüfen, ob der zugewiesene Speicher ausreicht – und ihn bei Bedarf zu vergrößern.

    int initialSize = 10000;
    bool spaceAllocated = false;

    while (!spaceAllocated)
    {
        // Erstellen des User Space mit der aktuellen Größe
        var initialParameters = new List<NTiProgramParameter>
        {
            new NTiProgramParameter("NTILOBJ", 10).Append("QTEMP", 10), // Name des User Space
            new NTiProgramParameter("QUSLOBJ", 10), // Erweiterungsname
            new NTiProgramParameter(initialSize), // Anfangsgröße
            new NTiProgramParameter(new byte[] { 0x00 }), // Initialisierungswert
            new NTiProgramParameter("*ALL", 10), // Attribute
            new NTiProgramParameter("List Object Information Userspace", 50) // Beschreibung
        };

        conn.CallProgram("QSYS", "QUSCRTUS", initialParameters);
    

4️⃣ Aufruf der QUSLOBJ-API nach Erstellung des User Space

Nachdem der User Space erstellt wurde, kann die API QUSLOBJ aufgerufen werden, um die Objektliste abzurufen. Die API erwartet mehrere Eingabeparameter, die korrekt formatiert sein müssen, um Fehler zu vermeiden.

Die QUSLOBJ-API benötigt Parameter im festen Char-Format:

  • Der User Space ist ein String mit 20 Zeichen (10 für den Namen, 10 für die Bibliothek) – daher .Append("QTEMP", 10) zur Verkettung
  • Das Datenformat ist ein String mit 8 Zeichen ("OBJL0400")
  • Objektname und Bibliothek bilden zusammen einen String mit 20 Zeichen (.Append(libraryName, 10))
  • Der Objekttyp ist ein String mit 10 Zeichen (*PGM, *FILE, usw.)
  • Schließlich ist die Fehlerstruktur ein String mit 16 Zeichen (hier leer)

Sobald diese Parameter definiert sind, wird das Programm aufgerufen und die API ausgeführt:

var parameters = new List<NTiProgramParameter>
{
    new NTiProgramParameter("NTILOBJ", 10).Append("QTEMP", 10), // User Space
    new NTiProgramParameter("OBJL0400", 8),  // Datenformat
    new NTiProgramParameter(objectName, 10).Append(libraryName, 10), // Objekt + Bibliothek
    new NTiProgramParameter(objectType, 10), // Objekttyp
    new NTiProgramParameter("", 16) //  Fehlerstruktur (optional)
};

conn.CallProgram("QSYS", "QUSLOBJ", parameters);

5️⃣ Abrufen der Ergebnisse

Sobald QUSLOBJ ausgeführt wurde, werden die Ergebnisse im User Space gespeichert. Um sie abzurufen, verwendet man die API QUSRTVUS, die den Inhalt des User Space ausliest.

  • Der erste Parameter bezeichnet immer den User Space (NTILOBJ in QTEMP)
  • Der zweite Parameter ist ein fester Wert (1), der das Abrufformat angibt
  • Der dritte Parameter entspricht der zuvor dynamisch definierten Größe des User Space
  • Der letzte Parameter ist ein leerer String mit der Größe des User Space, der als Ausgabe markiert ist (.AsOutput()), in den die Daten geschrieben werden
var finalParameters = new List<NTiProgramParameter>
{
    new NTiProgramParameter("NTILOBJ", 10).Append("QTEMP", 10),
    new NTiProgramParameter(1),
    new NTiProgramParameter(initialSize),
    new NTiProgramParameter("", initialSize).AsOutput() // Datenabruf
};

conn.CallProgram("QSYS", "QUSRTVUS", finalParameters);

6️⃣ Analyse der zurückgegebenen Daten

Nun müssen wir die im User Space gespeicherten Daten extrahieren und analysieren. Die Ergebnisse befinden sich im letzten Parameter, der beim Aufruf von QUSRTVUS als Output markiert wurde.

Wir lesen allgemeine Informationen aus: den Offset der Daten, die Anzahl der Einträge sowie die Größe jedes Eintrags. Diese Werte helfen uns zu verstehen, wo und wie wir die Liste der zurückgegebenen Objekte durchlaufen können. Die allgemeine Datenstruktur für List-APIs ist hier dokumentiert.

  • offsetToData gibt an, wo die Objektdaten im Speicher beginnen
  • numberOfEntries gibt an, wie viele Objekte zurückgegeben wurden
  • entrySize beschreibt, wie viele Bytes jedem Objekt zugewiesen sind
  • listSize ist die Gesamtgröße der zurückgegebenen Daten im User Space

Die API QUSLOBJ kann bei Fehlern einen CPFxxxx-Fehlercode zurückgeben. Dieser befindet sich in parameters[4], also im fünften Parameter unseres Aufrufs. Er muss bereinigt werden, um Steuerzeichen zu entfernen.

string error = new string(parameters[4].GetString(0, 16)
    .Where(c => !char.IsControl(c))
    .ToArray())
    .Trim();

var listOfParameters = finalParameters[3]; // 4. Parameter (0-basiert)
var offsetToData = listOfParameters.GetInt(0x7C);   // Daten-Offset
var numberOfEntries = listOfParameters.GetInt(0x84) // Anzahl der Einträge
var entrySize = listOfParameters.GetInt(0x88);  // Größe pro Eintrag
var listSize = listOfParameters.GetInt(0x80);     // Gesamtdatengröße

Anschließend prüfen wir, ob der reservierte Speicherplatz im User Space ausreicht. Wenn listSize+ offsetToData größer ist als initialSize, löschen wir den User Space, vergrößern ihn und starten den QUSLOBJ-Aufruf erneut:

var totalSize = listSize + offsetToData;

if (totalSize > initialSize)
{
    conn.ExecuteClCommand("DLTUSRSPC QTEMP/NTILOBJ");
    initialSize = totalSize;
}

Wenn der Speicherplatz ausreicht, wird die Objektliste durchlaufen, um die Informationen zu extrahieren. Jedes von der API zurückgegebene Objekt wird als Datenblock an einer genau definierten Position gespeichert. Die Struktur ist von IBM festgelegt und folgt einem strikten Aufbau, bei dem jedes Feld an einem bestimmten Offset beginnt und eine feste Länge besitzt.

Der Ansatz besteht darin, die Objektliste in einer Schleife zu durchlaufen, basierend auf der Anzahl zurückgegebener Einträge. In jeder Iteration berechnen wir die exakte Position des aktuellen Objekts, indem wir den Startoffset der Daten mit dem Index multipliziert mit der Eintragsgröße addieren. Dadurch gelangen wir direkt zu dem Objekt, das analysiert werden soll.

Für das Auslesen der Informationen verwenden wir Methoden, die je nach Datentyp angepasst sind:

  • GetString(offset, length).Trim() Für Textfelder mit fester Länge, wobei überflüssige Leerzeichen entfernt werden,
  • GetInt(offset) Für numerische Binärwerte (4 oder 8 Byte), ohne Angabe der Länge
  • GetDTSTimestamp(offset) NTi-spezifische Methode zur Umwandlung eines IBM i -Timestamps in ein verwertbares .NET-DateTime.

Sobald die Werte extrahiert wurden, werden sie in einem ListObjectsInformation -Objekt gespeichert und der Ergebnisliste hinzugefügt.

var result = new List<ListObjectsInformation>();

if (numberOfEntries > 0)
{
    for (int i = 0; i < numberOfEntries; i++)
    {

        // Berechnung des Offsets für den aktuellen Eintrag im User Space
        int currentOffset = offsetToData + (i * entrySize);

        result.Add(new ListObjectsInformation
        {
              ObjectName = listOfParameters.GetString(currentOffset, 10).Trim(),         // Offset 0, Länge 10
              LibraryName = listOfParameters.GetString(currentOffset + 10, 10).Trim(),   // Offset 10, Länge 10
              ObjectType = listOfParameters.GetString(currentOffset + 20, 10).Trim(),    // Offset 20, Länge 10
              InformationStatus = listOfParameters.GetString(currentOffset + 30, 1).Trim(), // Offset 30, Länge 1
              ExtendedAttribute = listOfParameters.GetString(currentOffset + 31, 10).Trim(), // Offset 31, Länge 10
              TextDescription = listOfParameters.GetString(currentOffset + 41, 50).Trim(),   // Offset 41, Länge 50
              UserDefinedAttribute = listOfParameters.GetString(currentOffset + 91, 10).Trim(), // Offset 91, Länge 10
              AspNumber = listOfParameters.GetInt(currentOffset + 108),                 // Offset 108, Binärwert
              Owner = listOfParameters.GetString(currentOffset + 112, 10).Trim(),       // Offset 112, Länge 10
              ObjectDomain = listOfParameters.GetString(currentOffset + 122, 2).Trim(), // Offset 122, Länge 2
              CreationDateTime = listOfParameters.GetDTSTimestamp(currentOffset + 124), // Offset 124, NTi-Timestamp
              ChangeDateTime = listOfParameters.GetDTSTimestamp(currentOffset + 132),   // Offset 132, NTi-Timestamp
              StorageStatus = listOfParameters.GetString(currentOffset + 140, 10).Trim() // Offset 140, Länge 1
        });
    }
}

Sobald alle Daten extrahiert und als C#-Objekte gespeichert wurden, sollte der temporäre User Space gelöscht werden. Obwohl dieser in QTEMP gespeichert wird (und somit am Ende der Session automatisch verschwindet), ist es empfohlen, ihn sofort zu löschen, um unnötige Last bei mehrfachen Aufrufen zu vermeiden.

Zum Löschen verwenden wir den CL-Befehl DLTUSRSPC. Danach gibt die Methode die Liste der extrahierten Objekte zurück. Falls kein Objekt gefunden wurde, wird eine leere Liste zurückgegeben – für saubere Ergebnisverarbeitung ohne Laufzeitfehler.

conn.ExecuteClCommand("DLTUSRSPC QTEMP/NTILOBJ");
return result;

Wenn keine Einträge gefunden wurden:

return new List<ListObjectsInformation>();

Fazit

Mit diesem letzten Schritt ist unsere Methode vollständig einsatzbereit. Sie folgt einem klar strukturierten Ablauf, der Folgendes ermöglicht:

  • Dynamisches Erstellen eines User Space
  • Aufruf der QUSLOBJ-API zur Objektauflistung
  • Extraktion der Daten gemäß der von IBM definierten Struktur
  • Dynamische Speicherverwaltung durch Größenanpassung bei Bedarf
  • Umwandlung der Ergebnisse in nutzbare C#-Objekte
  • Systematisches Löschen des temporären User Space nach der Nutzung
public static List<ListObjectsInformation> RetrieveObjectList(
    string libraryName,
    string objectName = "*ALL",
    string objectType = "*ALL"
    )
{
    int initialSize = 10000;
    bool spaceAllocated = false;

    List<ListObjectsInformation> listResult = new List<ListObjectsInformation>();
    string errorCode = null;

    while (!spaceAllocated)
    {
        // 1- Erstellung des User Space
        var initialParameters = new List<NTiProgramParameter>
        {
            new NTiProgramParameter("NTILOBJ", 10).Append("QTEMP", 10),
            new NTiProgramParameter("QUSLOBJ", 10),
            new NTiProgramParameter(initialSize),
            new NTiProgramParameter(new byte[] { 0x00 }),
            new NTiProgramParameter("*ALL", 10),
            new NTiProgramParameter("List Object Information Userspace", 50)
        };
        conn.CallProgram("QSYS", "QUSCRTUS", initialParameters);

        // 2- Aufruf der QUSLOBJ-API

        var parameters = new List<NTiProgramParameter>
        {
            new NTiProgramParameter("NTILOBJ", 10).Append("QTEMP", 10),
            new NTiProgramParameter("OBJL0400", 8),
            new NTiProgramParameter(objectName, 10).Append(libraryName, 10),
            new NTiProgramParameter(objectType, 10),
            new NTiProgramParameter("", 16)
        };
        conn.CallProgram("QSYS", "QUSLOBJ", parameters);

        // 3.  Abrufen der Daten

        var finalParameters = new List<NTiProgramParameter>
        {
            new NTiProgramParameter("NTILOBJ", 10).Append("QTEMP", 10),
            new NTiProgramParameter(1),
            new NTiProgramParameter(initialSize),
            new NTiProgramParameter("", initialSize).AsOutput()
        };
        conn.CallProgram("QSYS", "QUSRTVUS", finalParameters);

        string error = new string(parameters[4].GetString(0, 16).Where(c => !char.IsControl(c)).ToArray()).Trim();

        var listOfParameters = finalParameters[3];
        var offsetToData = listOfParameters.GetInt(0x7C);
        var numberOfEntries = listOfParameters.GetInt(0x84);
        var entrySize = listOfParameters.GetInt(0x88);
        var listSize = listOfParameters.GetInt(0x80);


        var totalSize = listSize + offsetToData;

        if(totalSize > initialSize)
        {
            conn.ExecuteClCommand("DLTUSRSPC QTEMP/NTILOBJ");
            initialSize = totalSize;
        }

        else
        {
            spaceAllocated = true;
            var result = new List<ListObjectsInformation>();

            if (numberOfEntries <= 0)
            {
                return new List<ListObjectsInformation>();
            }

            for(int i = 0; i < numberOfEntries; i++)
            {
                int currentOffset = offsetToData + (i * entrySize);

                result.Add(new ListObjectsInformation
                {
                    ObjectName = listOfParameters.GetString(currentOffset, 10).Trim(),
                    LibraryName = listOfParameters.GetString(currentOffset + 10 , 10).Trim(),
                    ObjectType = listOfParameters.GetString(currentOffset + 20, 10).Trim(),
                    InformationStatus = listOfParameters.GetString(currentOffset + 30, 1).Trim(),
                    ExtendedAttribute = listOfParameters.GetString(currentOffset + 31, 10).Trim(),
                    TextDescription = listOfParameters.GetString(currentOffset + 41, 50).Trim(),
                    UserDefinedAttribute = listOfParameters.GetString(currentOffset + 91, 10).Trim(),
                    AspNumber = listOfParameters.GetInt(currentOffset + 108),
                    Owner = listOfParameters.GetString(currentOffset + 112, 10).Trim(),
                    ObjectDomain = listOfParameters.GetString(currentOffset + 122, 2).Trim(),
                    CreationDateTime = listOfParameters.GetDTSTimestamp(currentOffset + 124),
                    ChangeDateTime = listOfParameters.GetDTSTimestamp(currentOffset + 132),
                    StorageStatus = listOfParameters.GetString(currentOffset + 140, 10).Trim(),
                    CompressionStatus = listOfParameters.GetString(currentOffset + 150 , 10).Trim(),
                    AllowChangeByProgram = listOfParameters.GetString(currentOffset + 151, 1).Trim(),
                    ChangedByProgram = listOfParameters.GetString(currentOffset + 152, 1).Trim(),
                    ObjectAuditing = listOfParameters.GetString(currentOffset + 153, 10).Trim(),
                    IsDigitallySigned = listOfParameters.GetString(currentOffset + 163, 1).Trim(),
                    IsSystemTrustedSigned = listOfParameters.GetString(currentOffset + 164, 1).Trim(),
                    HasMultipleSignatures = listOfParameters.GetString(currentOffset + 165, 1).Trim(),
                    LibraryAspNumber = listOfParameters.GetInt(currentOffset + 168),
                    SourceFileName = listOfParameters.GetString(currentOffset + 172, 10).Trim(),
                    SourceFileLibrary = listOfParameters.GetString(currentOffset + 182, 10).Trim(),
                    SourceFileMember = listOfParameters.GetString(currentOffset + 192, 10).Trim(),
                    SourceFileUpdatedDateTime = listOfParameters.GetString(currentOffset + 202, 13).Trim(),
                    CreatorUserProfile = listOfParameters.GetString(currentOffset + 215, 10).Trim(),
                    CreationSystem = listOfParameters.GetString(currentOffset + 225, 8).Trim(),
                    SystemLevel = listOfParameters.GetString(currentOffset + 233, 9).Trim(),
                    Compiler = listOfParameters.GetString(currentOffset + 242, 16).Trim(),
                    ObjectLevel = listOfParameters.GetString(currentOffset + 258, 8).Trim(),
                    IsUserChanged = listOfParameters.GetString(currentOffset + 266, 1).Trim(),
                    LicensedProgram = listOfParameters.GetString(currentOffset + 267, 16).Trim(),
                    PTF = listOfParameters.GetString(currentOffset + 283, 10).Trim(),
                    APAR = listOfParameters.GetString(currentOffset + 293, 10).Trim(),
                    PrimaryGroup = listOfParameters.GetString(currentOffset + 303, 10).Trim(),
                    IsOptimallyAligned = listOfParameters.GetString(currentOffset + 315, 1).Trim(),
                    PrimaryAssociatedSpaceSize = listOfParameters.GetInt(currentOffset + 316)
                });
            }
            conn.ExecuteClCommand("DLTUSRSPC QTEMP/NTILOBJ");
            return result;
        }
    }
    return new List<ListObjectsInformation>();
}

Quentin Destrade