QUSLOBJ IBM i: llamada a API del sistema en .NET/C# con NTi

Descubra cómo utilizar la API del sistema IBM i QUSLOBJ en .NET con NTi para listar dinámicamente los objetos de una biblioteca (*FILE, *PGM, etc.) desde una aplicación C#.

Imagen ilustrativa del artículo

IBM i ofrece numerosas APIs del sistema que permiten acceder a los recursos del sistema operativo. Algunas de estas APIs devuelven directamente datos utilizables en forma de estructuras o flujos, mientras que otras requieren el uso de un User Space para almacenar los resultados antes de poder procesarlos.

La API QUSLOBJ (List Objects) pertenece a esta segunda categoría: no devuelve directamente la lista de objetos como salida, sino que escribe los resultados en un User Space, una zona de memoria temporal. Este mecanismo es común en las APIs de IBM i que gestionan conjuntos de datos dinámicos, ya que permite recuperar grandes volúmenes de información sin una limitación estricta en el tamaño de los resultados.

El objetivo de este tutorial es explicarte cómo implementar correctamente un método con NTi para invocar esta API desde .NET, y cómo interpretar la documentación de IBM para construir adecuadamente las estructuras de entrada y analizar los datos devueltos.

El código fuente completo de la implementación está disponible al final de este tutorial.
Puedes seguir cada paso para entender el funcionamiento y luego consultarlo como referencia global o para uso directo.

Paso 1 - Elección de una API: QSYS.QUSLOBJ

Para utilizar la API QUSLOBJ, lo primero que hay que hacer es consultar su documentación. Los pasos principales a seguir son los siguientes:

  • Crear un User Space para almacenar los resultados mediante la API QUSCRTUS
  • Llamar a la API QUSLOBJ especificando los parámetros de entrada requeridos
  • Leer los resultados almacenados en el User Space usando la API QUSRTVUS
  • Recorrer los objetos devueltos y convertirlos en objetos .NET

Paso 2 - Lectura de la documentación de IBM

Antes de implementar la llamada a QUSLOBJ, es importante entender qué parámetros espera esta API y qué devuelve como salida.

###📥Parámetros de entrada:

Parámetro Tipo Dirección Descripción
User Space Char(20) Input Nombre (10 caracteres) y biblioteca (10 caracteres) donde almacenar la lista
Format Name Char(8) Input Formato de los datos (por ejemplo, OBJL0400 en nuestro caso)
Object & LibraryName Char(20) Input Nombre del objeto (10 caracteres) y biblioteca (10 caracteres)
Object Type Char(10) Input Tipo de objeto (*FILE, *PGM, etc.)
Error Code Char(* ) Input / Output Estructura de error (opcional)

###📥Parámetros de salida:

La API QUSLOBJ no devuelve directamente la lista de objetos como parámetros de salida. En su lugar, escribe los resultados en un User Space, una zona de memoria temporal que debemos especificar como entrada.

Un User Space es un objeto del sistema utilizado para almacenar grandes cantidades de datos, especialmente los resultados devueltos por algunas APIs de IBM i.

Este espacio se crea antes de llamar a QUSLOBJ, utilizando la API QUSCRTUS. Una vez ejecutada la llamada, los objetos listados se escriben en ese User Space. Para recuperar los resultados, se utiliza la API QUSRTVUS, que permite leer su contenido.

No entraremos aquí en los detalles de la implementación, pero recuerde que todos los datos devueltos se almacenan en este User Space y deben analizarse respetando la estructura definida por IBM.

Paso 3 - Implementación del método de llamada a QUSLOBJ con NTi

Ahora que sabemos cómo funciona QUSLOBJ y cómo interpretar los resultados almacenados en el User Space, vamos a proceder con la implementación de nuestro método en C# usando NTi.

1️⃣ Definición del modelo de datos

Antes que nada, debemos definir un modelo de datos que represente cada objeto devuelto por la API. Usamos el formato OBJL0400 para recuperar información detallada sobre los objetos. Por lo tanto, creamos una clase C# que refleje esta estructura:

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️⃣ Definición del método principal

Definimos el método que construiremos para recuperar una lista de objetos desde una biblioteca determinada. La API espera como parámetros libraryName, objectName y objectType.

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

3️⃣ Creación del User Space

Antes de llamar a QUSLOBJ, se debe crear un User Space en QTEMP, como se explicó anteriormente, que nos permitirá recuperar la lista de objetos generada. La API QUSCRTUS se utiliza para crear este User Space. Hay que proporcionarle varios parámetros, entre ellos:

  • El nombre del User Space, en este caso NTILOBJ en QTEMP
  • Un tamaño inicial que se ajustará dinámicamente en función de los datos devueltos
  • Un valor de inicialización, en este caso 0x00, que indica un espacio en blanco
  • Un atributo genérico y una descripción

Dado que el tamaño de los datos devueltos no se conoce de antemano, se utiliza un bucle "while" para verificar si el espacio asignado es suficiente, y aumentarlo si es necesario:

    int initialSize = 10000;
    bool spaceAllocated = false;

    while (!spaceAllocated)
    {
        // Creación del User Space con el tamaño actual
        var initialParameters = new List<NTiProgramParameter>
        {
            new NTiProgramParameter("NTILOBJ", 10).Append("QTEMP", 10), //  Nombre del User Space
            new NTiProgramParameter("QUSLOBJ", 10), // Nombre de extensión
            new NTiProgramParameter(initialSize), // Tamaño inicial
            new NTiProgramParameter(new byte[] { 0x00 }), // Valor de inicialización
            new NTiProgramParameter("*ALL", 10), // Atributos
            new NTiProgramParameter("List Object Information Userspace", 50) // Descripción
        };

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

4️⃣ Llamada a la API QUSLOBJ tras la creación del User Space

Una vez creado el User Space, podemos llamar a la API QUSLOBJ para recuperar la lista de objetos. Esta API espera varios parámetros de entrada, que deben estar correctamente formateados para evitar errores.

La API QUSLOBJ requiere parámetros en forma de cadenas de caracteres (Char) de longitud fija:

  • El User Space es una cadena de 20 caracteres (10 para el nombre, 10 para la biblioteca), de ahí el uso de .Append("QTEMP", 10) para concatenar
  • El formato de datos es una cadena de 8 caracteres ("OBJL0400")
  • El nombre del objeto y la biblioteca forman una cadena de 20 caracteres (.Append(libraryName, 10))
  • El tipo de objeto es una cadena de 10 caracteres (*PGM, *FILE, etc.)
  • Por último, la estructura de error es una cadena de 16 caracteres, vacía en este caso

Una vez definidos estos parámetros, se llama al programa y se ejecuta la API:

var parameters = new List<NTiProgramParameter>
{
    new NTiProgramParameter("NTILOBJ", 10).Append("QTEMP", 10), // User Space
    new NTiProgramParameter("OBJL0400", 8), // Formato de datos
    new NTiProgramParameter(objectName, 10).Append(libraryName, 10), // Objeto y biblioteca
    new NTiProgramParameter(objectType, 10), // Tipo de objeto
    new NTiProgramParameter("", 16) // Estructura de error (opcional)
};

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

5️⃣ Recuperación de los resultados

Una vez ejecutada la API QUSLOBJ, los resultados se almacenan en el User Space. Para recuperarlos, se utiliza la API QUSRTVUS, que permite leer su contenido.

  • El primer parámetro siempre hace referencia al User Space (NTILOBJ en QTEMP)
  • El segundo parámetro es un valor fijo (1), que indica el formato de recuperación
  • El tercer parámetro corresponde al tamaño del User Space asignado, definido dinámicamente anteriormente
  • El último parámetro es una cadena vacía del mismo tamaño que el User Space, marcada como salida mediante .AsOutput(), donde se escribirán los datos
var finalParameters = new List<NTiProgramParameter>
{
    new NTiProgramParameter("NTILOBJ", 10).Append("QTEMP", 10),
    new NTiProgramParameter(1),
    new NTiProgramParameter(initialSize),
    new NTiProgramParameter("", initialSize).AsOutput() //  Recuperación de los datos
};

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

6️⃣ Análisis de los datos devueltos

Ahora debemos extraer y analizar los datos contenidos en el User Space. Los resultados se devuelven en el último parámetro definido como salida en la llamada a QUSRTVUS.

Recuperamos las informaciones generales: el offset de inicio de los datos, el número de entradas, y el tamaño de cada entrada. Estos valores nos permiten entender dónde y cómo recorrer la lista de objetos devueltos. La estructura general de los datos para las APIs de tipo lista está documentada aquí.

  • El offset del inicio de los datos offsetToData indica dónde comienza realmente la lista de objetos
  • El número de entradas devueltas numberOfEntries indica cuántos objetos hay en la lista
  • El tamaño de una entrada entrySize nos permite saber cuántos bytes se asignan a cada objeto
  • El tamaño total de los datos listSize corresponde al espacio ocupado dentro del User Space

La API QUSLOBJ puede devolver un código de error del tipo CPFxxxx si se encuentra un problema. Este código se almacena en parameters[4], que corresponde al quinto parámetro de nuestra llamada. Por tanto, debemos extraerlo y limpiarlo para eliminar caracteres de control.

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

var listOfParameters = finalParameters[3]; // 4º parámetro (indexado desde 0)
var offsetToData = listOfParameters.GetInt(0x7C); // Offset del inicio de los datos
var numberOfEntries = listOfParameters.GetInt(0x84);  // Número de entradas devueltas
var entrySize = listOfParameters.GetInt(0x88); // Tamaño de cada entrada
var listSize = listOfParameters.GetInt(0x80);  // Tamaño total de los datos

A continuación, se verifica que el espacio asignado para el User Space sea suficiente. Si el tamaño total de los datos listSize + offsetToData supera el tamaño inicialmente asignado, se debe eliminar el User Space, aumentar su tamaño y volver a lanzar la llamada a QUSLOBJ.

var totalSize = listSize + offsetToData;

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

Si el espacio es suficiente, se recorre la lista de objetos para extraer su información. Cada objeto devuelto por la API está almacenado en bloques de datos en una posición exacta. La estructura está definida por IBM y sigue una disposición estricta, donde cada campo comienza en un offset específico y tiene una longitud fija.

El enfoque consiste en recorrer la lista de objetos en un bucle, basándose en el número de entradas devueltas. En cada iteración, se calcula la posición exacta del objeto actual sumando con el índice multiplicado por el tamaño de una entrada. Esto permite apuntar directamente al objeto que se quiere analizar.

Para extraer la información, utilizamos métodos adaptados según el tipo de dato:

  • GetString(offset, length).Trim() para campos de texto, extrae una cadena de longitud fija eliminando los espacios innecesarios.
  • GetInt(offset) para valores numéricos binarios almacenados en 4 u 8 bytes, sin necesidad de especificar longitud.
  • GetDTSTimestamp(offset) es un método específico de NTi que permite convertir un timestamp de IBM i en una fecha utilizable en .NET.

Una vez extraídos los valores, se almacenan en un objeto ListObjectsInformation y luego se añaden a la lista de resultados.

var result = new List<ListObjectsInformation>();

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

        // Cálculo del offset de la entrada actual dentro del User Space
        int currentOffset = offsetToData + (i * entrySize);

        result.Add(new ListObjectsInformation
        {
             ObjectName = listOfParameters.GetString(currentOffset, 10).Trim(),              // En el offset 0, longitud 10
             LibraryName = listOfParameters.GetString(currentOffset + 10, 10).Trim(),        // En el offset 10, longitud 10
             ObjectType = listOfParameters.GetString(currentOffset + 20, 10).Trim(),         // En el offset 20, longitud 10
             InformationStatus = listOfParameters.GetString(currentOffset + 30, 1).Trim(),   // En el offset 30, longitud 1
             ExtendedAttribute = listOfParameters.GetString(currentOffset + 31, 10).Trim(),  // En el offset 31, longitud 10
             TextDescription = listOfParameters.GetString(currentOffset + 41, 50).Trim(),    // En el offset 41, longitud 50
             UserDefinedAttribute = listOfParameters.GetString(currentOffset + 91, 10).Trim(), // En el offset 91, longitud 10
             AspNumber = listOfParameters.GetInt(currentOffset + 108),                       // En el offset 108, binario 4
             Owner = listOfParameters.GetString(currentOffset + 112, 10).Trim(),             // En el offset 112, longitud 10
             ObjectDomain = listOfParameters.GetString(currentOffset + 122, 2).Trim(),       // En el offset 122, longitud 2
             CreationDateTime = listOfParameters.GetDTSTimestamp(currentOffset + 124),       // En el offset 124, timestamp específico de NTi
             ChangeDateTime = listOfParameters.GetDTSTimestamp(currentOffset + 132),         // En el offset 132, timestamp específico de NTi
             StorageStatus = listOfParameters.GetString(currentOffset + 140, 10).Trim()      // En el offset 140, longitud 10
        });
    }
}

Una vez que todos los datos han sido extraídos y almacenados en forma de objetos C#, es importante liberar la memoria eliminando el User Space temporal. Aunque el User Space esté ubicado en QTEMP (y por lo tanto se elimine automáticamente al final de la sesión), es preferible eliminarlo inmediatamente después de su uso, para evitar una sobrecarga innecesaria en caso de llamadas repetidas.

Utilizamos el comando CL DLTUSRSPC para eliminar el User Space especificado. Una vez realizada esta eliminación, el método devuelve la lista de objetos extraídos. Si la API no ha devuelto ningún objeto, se retorna una lista vacía, a fin de garantizar una gestión limpia de los resultados y evitar posibles errores en los tratamientos posteriores.

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

Si no se ha recuperado ninguna entrada, se devuelve una lista vacía:

return new List<ListObjectsInformation>();

Conclusión

Con este último paso, nuestro método está completamente funcional. Sigue un proceso estructurado que permite:

  • La creación dinámica de un User Space
  • La llamada a QUSLOBJ para recuperar la lista de objetos
  • La extracción de los datos respetando la estructura definida por IBM
  • La gestión dinámica de la memoria ajustando el tamaño del User Space si es necesario
  • La transformación de los resultados en objetos C# utilizables
  • La eliminación sistemática del User Space después de su uso
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- Creación del 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- Llamada a la API QUSLOBJ

        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. Recuperación de los datos

        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