1 - Variables en el lenguaje COBOL
Variables en el lenguaje COBOL
Cualquier programa COBOL puede ser transformado en un microservicio y desplegado en Kubernetes.
Para ello, simplemente debe compilarse el programa y generar un módulo ejecutable que pueda ser invocado de manera dinámica o estática desde otro programa de aplicación. Puede consultar los ejemplos para aprender a realizar llamadas tanto dinámicas como estáticas.
De manera análoga al comportamiento de una función en un lenguaje de programación moderno, estos programas o subrutinas pueden recibir un conjunto de variables como parámetros de entrada/salida.
El procedimiento es sencillo, se debe codificar la cláusula USING
en la PROCEDURE DIVISION
replicado el número, orden y tipo de las variables utilizadas en la llamada realizada desde el programa principal.
En el caso de los programas Batch, el programa principal (asociado a una ficha STEP de un JCL) no suele utilizar parámetros (PARM) para la recepción de datos de entrada al programa. Los programas generalmente utilizan la sentencia COBOL ACCEPT
o leen la información necesaria directamente desde un fichero.
En el caso de los programas Online, el programa principal se asocia a una transacción y es el gestor transaccional (CICS o IMS) el encargado de invocar dicho programa. Este programa suele utilizar las sentencias correspondientes del gestor transaccional para recibir un mensaje compuesto por diferentes variables (EXEC CICS RECEIVE - GN) sobre un área de memoria previamente definida.
Independientemente del modo de ejecución (Online o Batch), estos programas COBOL principales pueden a su vez realizar distintas llamadas a subrutinas COBOL mediante la sentencia CALL (en el caso de utilizar CICS esa llamada podría realizarse mediante la sentencia EXEC CICS LINK).
Por tanto, si queremos exponer un programa COBOL como un microservicio que pueda ser invocado desde otros microservicios escritos en distintos lenguajes de programación es necesario entender los distintos tipos de datos que puede utilizar un programa COBOL y convertirlos a un tipo estándar utilizable desde cualquier lenguaje de programación.
Tipos de variables en COBOL
Definir una variable en un programa COBOL puede resultar confuso para alguien acostumbrado a trabajar con lenguajes de programación actuales. Adicionalmente, existen distintos tipos de datos, ampliamente utilizados por un programador COBOL, que no tienen equivalencia en cualquier lenguaje de programación moderno.
Vamos a intentar descifrar el funcionamiento de los tipos de datos COBOL más utilizados en un programa estándar y a definir su traducción a un lenguaje moderno como Go.
Se excluyen variables que requieren caracteres de doble byte
Como en cualquier otro lenguaje, para definir una variable, debemos declarar su nombre y su tipo (string, int, float, bytes, etc.), sin embargo en COBOL esa definición no es directa, se realiza mediante las cláusulas USAGE
y PICTURE
.
01 VAR1 PIC S9(3)V9(2) COMP VALUE ZEROES.
Mediante la cláusula USAGE
definiremos el tipo de almacenamiento interno que utilizará la variable.
La cláusula PICTURE
(o PIC
) define la máscara asociada a la variable y sus características generales, esta definición se realiza mediante la utilización de un conjunto de caracteres o símbolos específicos:
- A, la variable contiene caracteres alfabéticos
- X, la variable contiene caracteres alfanuméricos
- 9, variable numérica
- S, indica una variable numérica con signo
- V, número de posiciones decimales en una variable numérica
- Etc.
USAGE DISPLAY
Las variables definidas como USAGE DISPLAY
puede ser de los siguientes tipos:
Alfabéticas
Se definen mediante la utilización del símbolo A en la cláusula PICTURE
.
01 VAR-ALPHA PIC A(20).
Sólo pueden almacenar caracteres del alfabeto latino. Su uso no es muy común, siendo generalmente sustituidas por variables alfanuméricas que veremos a continuación.
Alfanuméricas
Se definen mediante la utilización del símbolo X en la cláusula PICTURE
.
01 VAR-CHAR PIC X(20).
Como en el anterior caso no es necesario definir la cláusula USAGE
, ya que las variables de tipo A o X por defecto son de tipo DISPLAY
.
Numéricas
Se definen mediante la utilización del símbolo 9 en la cláusula PICTURE
.
01 VAR1 PIC S9(3)V9(2) USAGE DISPLAY.
En este caso estamos definiendo una variable numérica de longitud 5 (3 posiciones enteras y 2 decimales) con signo.
La definición de variables numéricas de tipo DISPLAY
puede hacerse de manera explícita como en el ejemplo anterior o de manera implícita en caso de no codificar la cláusula USAGE
.
Almacenamiento interno
Cada uno de los caracteres definidos se almacena en un byte (EBCDIC), en el caso de variables de tipo numérico con signo, este se define en los primeros 4 bits del último byte.
Vamos a ver un ejemplo para entender mejor el funcionamiento de este tipo de variables.
Número | Valor EBCDIC |
---|---|
0 | x’F0' |
1 | x’F1' |
2 | x’F2' |
3 | x’F3' |
4 | x’F4' |
5 | x’F5' |
6 | x’F6' |
7 | x’F7' |
8 | x’F8' |
9 | x’F9' |
Si le asignamos un valor de 12345 a una variable de tipo
PIC 9(5) USAGE DISPLAY
Está ocuparía 5 bytes
12345 = x’F1F2F3F4F5’
En el caso de variables con signo
PIC S9(5) USAGE DISPLAY
+12345 = x’F1F2F3F4C5’
-12345 = x’F1F2F3F4D5’
USAGE COMP-1 o COMPUTATIONAL-1
Variable de tipo float de 32 bits (4 bytes).
USAGE COMP-2 o COMPUTATIONAL-2
Variable de tipo float de 64 bits (8 bytes).
Tanto en el caso de variables tipo COMP-1
como tipo COMP-2
, no se puede definir una cláusula PICTURE
asociada a dicha variable.
USAGE COMP-3 o COMPUTATIONAL-3
Variable numérica de tipo Packed-Decimal.
01 VAR-PACKED PIC S9(3)V9(2) USAGE COMP-3.
Se utilizan 4 bits para almacenar cada uno de los caracteres numéricos de la variable, el signo se almacena en los últimos 4 bits.
Por lo general, este tipo de variables se definen de longitud impar, para ocupar por completo el número de bytes utilizados.
Una sencilla regla para calcular el almacenamiento ocupado por una variable tipo COMP-3
es dividir la longitud total de la variable (posiciones enteras + posiciones decimales) entre 2 y sumarle 1, en el ejemplo anterior el almacenamiento requerido para la variable VAR-PACKED
5 / 2 = 2 -> longitud total 5 (3 enteros + 2 decimales)
2 + 1 = 3 -> almacenamiento requerido 3 bytes
Vamos a ver un ejemplo para comprender mejor el funcionamiento de este tipo de variables.
Vamos a asignarle el valor 12345, como en el ejemplo anterior, a una variable de tipo COMP-3
.
PIC S9(5) USAGE COMP-3.
Partimos del número en formato DISPLAY
x’F1F2F3F4F5’
Eliminamos los primeros 4 bits de cada byte y añadimos el signo al final
+12345 = x’12345C’
-12345 = x’12345D’
USAGE COMP-4 o COMPUTATIONAL-4 o COMP o BINARY
En este caso, el tipo de dato es numérico binario. Los números negativos se representan como complemento a 2.
El tamaño del almacenamiento requerido depende de la cláusula PICTURE
.
PICTURE | Almacenamiento | Valor |
---|---|---|
PIC S9(1) - S9(4) | 2 bytes | -32,768 hasta +32,767 |
PIC S9(5) - S9(9) | 4 bytes | -2,147,483,648 hasta +2,147,483,647 |
PIC S9(10) - S9(18) | 8 bytes | -9,223,372,036,854,775,808 hasta +9,223,372,036,854,775,807 |
PIC 9(1) - 9(4) | 2 bytes | 0 hasta 65,535 |
PIC 9(5) - 9(9) | 4 bytes | 0 hasta 4,294,967,295 |
PIC 9(10) - 9(18) | 8 bytes | 0 hasta 18,446,744,073,709,551,615 |
Hasta aquí el funcionamiento de este tipo de variables sería equivalente a la representación de variables enteras en la mayoría de lenguajes de programación (variables short, long o double y sus equivalentes sin signo ushort, ulong, udouble), sin embargo mediante el uso de la cláusula PICTURE
podemos limitar los valores máximos de dicha variable y definir una máscara decimal.
Por ejemplo:
01 VAR-COMP PIC 9(4)V9(2) USAGE COMP.
Utiliza 4 bytes de almacenamiento (int32), pero su valor está limitado desde 0 a 9999.99.
¿Qué sentido tiene definir variables enteras y posteriormente definir una máscara fija con el número de posiciones enteras y decimales?
Limitar el uso de operaciones con coma flotante. Hoy en día esto puede resultar extraño, pero tenía sentido hace 30 años para reducir los ciclos de CPU utilizados cuando el coste de computación era extremadamente caro.
Por otro lado, la práctica totalidad de las operaciones realizadas por instituciones financieras con moneda no requieren cálculos complejos y al operar con números enteros no perdemos precisión en caso de necesitar posiciones decimales (i.e. céntimos).
USAGE COMP-5 o COMPUTATIONAL-5
Este tipo de dato también es conocido como binario nativo.
Es equivalente a COMP-4, sin embargo los valores a almacenar no están limitados por la máscara definida en la cláusula PICTURE
.
PICTURE | Almacenamiento |
---|---|
PIC S9(4) COMP-5 | short (int16) |
PIC S9(9) COMP-5 | long (int32) |
PIC S9(18) COMP-5 | double (int64) |
PIC 9(4) COMP-5 | ushort (uint16) |
PIC 9(9) COMP-5 | ulong (uint32) |
PIC 9(18) COMP-5 | udouble (uint64) |
Conversión de variables a un tipo estándar
Es importante comentar que solo es necesario convertir las variables expuestas por los programas principales, una vez inicializado el runtime de cobol utilizado y ejecutado el programa principal, las llamadas entre programas COBOL realizadas mediante la sentencia CALL son responsabilidad del runtime.
Un caso especial es la llamada entre programas COBOL, desplegados en distintos contenedores. Para ello se ha diseñado un mecanismo específico, similar al funcionamiento de la sentencia LINK del gestor transaccional CICS.
Puede consultar cómo realizar llamadas entre programas COBOL desplegados en distintos contenedores aplicativos en el apartado de ejemplos d8link.
Como norma general, cualquier variable COBOL podría ser expuesta de distintas formas (int, float, string, etc.) y ser convertida posteriormente, sin embargo y para simplificar y optimizar el proceso de conversión vamos a utilizar un conjunto de reglas sencillas que describiremos a continuación.
EBCDIC
Como hemos comentado anteriormente, el mainframe IBM utiliza EBCDIC internamente.
Para intentar replicar al máximo el comportamiento de los programas COBOL entre plataformas, algunos fabricantes permiten seguir utilizando EBCDIC internamente para el manejo de datos de aplicación.
Si bien, a corto plazo esta estrategia podría facilitar la migración del código, presenta enormes problemas de compatibilidad, evolución y soporte a medio largo plazo. Por tanto, los programas COBOL migrados utilizarán caracteres ASCII internamente.
Es necesario identificar los programas que hagan uso de instrucciones COBOL que manejan caracteres hexadecimales y reemplazar dichas cadenas de caracteres
MOVE X’F1F2F3F4F5’ TO VAR-NUM1.
Big-endian vs little-endian
La plataforma mainframe IBM es big-endian, el byte más significativo estaría almacenado en la dirección de memoria de menor valor, o dicho de otro modo el signo se almacenaría en el primer bit de la izquierda.
Por el contrario la plataforma x86 y arm utilizan little-endian para la representación de variables binarias.
Como en el caso anterior, pensamos que la mejor estrategia es utilizar de manera nativa la arquitectura de destino, por tanto los programas serán compilados para utilizar little-endian.
Tamaño máximo variables COMP
Como regla general, el tamaño máximo de una variable numérica es 18 posiciones, independientemente del tipo utilizado (DISPLAY
, COMP
, COMP-3
, COMP-5
).
Existe sin embargo, la capacidad en la plataforma mainframe de extender ese límite hasta 31 dígitos en el caso de algún tipo de variable (por ejemplo COMP-3
).
Estamos hablando de variables numéricas utilizadas para realizar operaciones aritméticas, salvo en el caso concreto de algún país con episodios de hiperinflación mantenidos durante un largo periodo de tiempo, el límite de 18 dígitos es suficiente.
Variables binarias
Aunque no es habitual utilizar variables de tipo binario con máscara decimal (COMP
o COMP-4
), pueden utilizarse para operar en binario números de gran tamaño o precisión y evitar así el uso de campos tipo float.
En el caso de variables de tipo COMP-5
o binarias nativas, no tiene sentido aplicar una máscara decimal, por tanto su implementación será directa a un tipo de variable entera correspondiente a su tamaño.
Modelo de conversión
Tipo COBOL | Tipo Go |
---|---|
PIC X(n) | string |
COMP-1 | float32 |
COMP-2 | float64 |
PIC S9(1 hasta 4) COMP-5 | int16 |
PIC S9(5 hasta 9) COMP-5 | int32 |
PIC S9(10 hasta 18) COMP-5 | int64 |
PIC 9(1 hasta 4) COMP-5 | uint16 |
PIC 9(5 hasta 9) COMP-5 | uint32 |
PIC 9(10 hasta 18) COMP-5 | uint64 |
En COBOL no existe el concepto de String, el tamaño de la variable
PIC X(n)
corresponde siempre y exactamente a la longitud definida en la cláusulaPIC
.En caso de que el tamaño del string sea menor al tamaño definido en COBOL, será necesario justificar con espacios por la derecha.
Hasta aquí las variables que presentan una equivalencia con algún tipo de variable del lenguaje Go, a continuación vamos a definir el comportamiento de los tipos de datos numéricos con máscara decimal.
Estos son los tipos de datos generalmente utilizados por los programadores COBOL. En el caso de que el programa quiera ser expuesto para su invocación desde una plataforma externa, suelen utilizarse variables de tipo DISPLAY
o zoned decimal para facilitar la conversión de datos (ASCII-EBCDIC) entre plataformas y facilitar la depuración de errores.
En nuestro caso, para simplificar el manejo de datos, todas estas variables se expondrán como tipo string
.
Tipo COBOL | Tipo Go |
---|---|
PIC S9(n) | string |
PIC S9(n) COMP-3 | string |
PIC S9(n) COMP o COMP-4 o BINARY | string |
El programa que realiza la llamada deberá asegurar que el dato enviado corresponde a un valor numérico.
Las variables recibidas deberán ajustarse a la máscara definida en la cláusula
PIC
, respetando el número de posiciones enteras y decimales y justificando con ceros por la izquierda en caso de ser necesario.
Proceso de conversión
Vamos a partir de un sencillo ejemplo, un programa COBOL que recibe una estructura de datos con distintos tipos de variables.
******************************************************************
IDENTIFICATION DIVISION.
PROGRAM-ID. vars.
ENVIRONMENT DIVISION.
CONFIGURATION SECTION.
DATA DIVISION.
FILE SECTION.
WORKING-STORAGE SECTION.
* Declare variables in the WORKING-STORAGE section
LINKAGE SECTION.
* Data to share with COBOL subroutines
01 MY-RECORD.
10 CHAR PIC X(09) VALUE SPACES.
10 COMP2 COMP-2 VALUE ZEROES.
10 COMP1 COMP-1 VALUE ZEROES.
10 COMP5D PIC S9(18) COMP-5 VALUE ZEROES.
10 COMP5L PIC S9(9) COMP-5 VALUE ZEROES.
10 COMP5S PIC S9(4) COMP-5 VALUE ZEROES.
10 COMP5UD PIC 9(18) COMP-5 VALUE ZEROES.
10 COMP5UL PIC 9(9) COMP-5 VALUE ZEROES.
10 COMP5US PIC 9(4) COMP-5 VALUE ZEROES.
10 NDISPLAY PIC S9(3)V9(2) VALUE ZEROES.
10 COMP3 PIC S9(3)V9(2) COMP-3 VALUE ZEROES.
10 COMP4D PIC S9(8)V9(2) COMP VALUE ZEROES.
10 COMP4L PIC S9(3)V9(2) COMP VALUE ZEROES.
10 COMP4S PIC S9(2)V9(2) COMP VALUE ZEROES.
PROCEDURE DIVISION USING BY REFERENCE MY-RECORD.
* code goes here!
DISPLAY "char: " CHAR.
DISPLAY "comp-2: " COMP2.
DISPLAY "comp-1: " COMP1.
DISPLAY "comp5 double: " COMP5D.
DISPLAY "comp5 long: " COMP5L.
DISPLAY "comp5 short: " COMP5S.
DISPLAY "comp5 Udouble: " COMP5UD.
DISPLAY "comp5 Ulong: " COMP5UL.
DISPLAY "comp5 Ushort: " COMP5US.
DISPLAY "display: " NDISPLAY.
DISPLAY "comp-3: " COMP3.
DISPLAY "comp double: " COMP4D.
DISPLAY "comp long: " COMP4L.
DISPLAY "comp short: " COMP4S.
MOVE 0 TO RETURN-CODE.
GOBACK.
A continuación analizaremos dicha estructura (se puede parsear de manera automática) y generaremos dos ficheros:
Un fichero de configuración (vars.yaml) con las características de las variables COBOL presentes en la estructura a convertir (nombre, tipo de variable, longitud, posiciones decimales, signo).
type = 0 -> Variable numérica de tipo
DISPLAY
type = 1 -> Variable numérica de tipo
COMP-1
type = 2 -> Variable numérica de tipo
COMP-2
type = 3 -> Variable numérica de tipo
COMP-3
type = 4 -> Variable numérica de tipo
COMP-4 o BINARY o COMP
type = 5 -> Variable numérica de tipo
COMP-5
type = 9 -> Variable alfanumérica de tipo
CHAR
---
copy:
- field: "Char"
type: 9
length: 9
decimal: 0
sign: false
- field: "Comp2"
type: 2
length: 0
decimal: 0
sign: true
- field: "Comp1"
type: 1
length: 0
decimal: 0
sign: true
- field: "Comp5Double"
type: 5
length: 18
decimal: 0
sign: true
- field: "Comp5Long"
type: 5
length: 9
decimal: 0
sign: true
- field: "Comp5Short"
type: 5
length: 4
decimal: 0
sign: true
- field: "Comp5Udouble"
type: 5
length: 18
decimal: 0
sign: false
- field: "Comp5Ulong"
type: 5
length: 9
decimal: 0
sign: false
- field: "Comp5Ushort"
type: 5
length: 4
decimal: 0
sign: false
- field: "NumDisplay"
type: 0
length: 5
decimal: 2
sign: true
- field: "Comp3"
type: 3
length: 5
decimal: 2
sign: true
- field: "CompDouble"
type: 4
length: 10
decimal: 2
sign: true
- field: "CompLong"
type: 4
length: 5
decimal: 2
sign: true
- field: "CompShort"
type: 4
length: 4
decimal: 2
sign: true
Un fichero (request.go) con la representación en Go de la estructura COBOL.
package request
type Request struct {
Char string
Comp2 float64
Comp1 float32
Comp5Double int64
Comp5Long int32
Comp5Short int16
Comp5Udouble uint64
Comp5Ulong uint32
Comp5Ushort uint16
NumDisplay string
Comp3 string
CompDouble string
CompLong string
CompShort string
}
Y por último, un fichero (response.go). Podemos copiar la estructura utilizada anteriormente ya que los parámetros utilizados por el programa son de entrada/salida.
package response
type Response struct {
Char string
Comp2 float64
Comp1 float32
Comp5Double int64
Comp5Long int32
Comp5Short int16
Comp5Udouble uint64
Comp5Ulong uint32
Comp5Ushort uint16
NumDisplay string
Comp3 string
CompDouble string
CompLong string
CompShort string
}
Ya tenemos todo lo necesario para ejecutar nuestro programa COBOL.
Ejecución programa de prueba
A continuación se explica cómo ejecutar el programa de pruebas anterior.
Los ejemplos pueden descargarse directamente del repo de GitHub
La estructura de directorios del ejemplo (d8vars), es la siguiente:
├── cmd
├── cobol
├── conf
├── internal
│ └── cgocobol
│ └── common
│ └── service
├── model
│ └── request
│ └── response
├── test
│ go.mod
│ go.sum
| Dockerfile
/cobol
Contiene los programas COBOL compilados que se vayan a ejecutar (*.dylib o *.so).
Utilice las siguientes opciones de compilación para definir el comportamiento requerido en los campos de tipo binario.
cobc -m vars.cbl -fbinary-byteorder=native -fbinary-size=2-4-8
/conf
Ficheros de configuración descritos anteriormente, utilizados para describir la estructura de la COPY COBOL.
/model
Contiene la definición de las estructuras de datos en Go (request/response).
/internal
En este caso, podemos encontrar una versión simplificada del código necesario para la conversión de datos entre lenguajes y la ejecución de los programas COBOL.
/test
Por último, el directorio test contiene una utilidad para generar datos de prueba aleatorios de acuerdo a los tipos de datos definidos en el programa COBOL.
Para ejecutar el código sólo hace falta ir al directorio /cmd, abrir un terminal y escribir
go run .
Recuerde definir al runtime del lenguaje COBOL, el directorio donde se encuentran los módulos a ejecutar
export COB_LIBRARY_PATH=/User/my_dir/d8vars/cobol
Puede utilizar el programa COBOL de ejemplo loancalc.cbl para realizar pruebas adicionales, simplemente compile el programa y modifique los ficheros de configuración y estructura de datos.
Modifique el fichero app.env para definir el nombre del programa COBOL a ejecutar y el nombre de la COPY COBOL a convertir.
COBOL_PROGRAM="loancalc"
COBOL_CONFIG="loancalc.yaml"
Copie el fichero loancalc.yml a la carpeta conf
---
copy:
- field: "PrincipalAmount"
type: 0
length: 7
decimal: 0
sign: true
- field: "InterestRate"
type: 0
length: 4
decimal: 2
sign: true
- field: "TimeYears"
type: 0
length: 2
decimal: 0
sign: true
- field: "Payment"
type: 0
length: 9
decimal: 2
sign: true
- field: "ErrorMsg"
type: 9
length: 20
decimal: 0
sign: false
Y sustituya las estructuras request.go y response.go por las siguientes:
package request
type Request struct {
PrincipalAmount string
InterestRate string
TimeYears string
}
package response
type Response struct {
Payment string
ErrorMsg string
}