Calling COBOL containers
De manera equivalente al mecanismo del CICS para llamar a programas remotos (EXEC CICS LINK), realice llamadas entre programas COBOL desplegados en distintos contenedores.
A continuación se describe gráficamente el flujo de ejecución
loanmain.cbl <–> d8link.go <———————–> main.go <–> loancalc.cbl
- El programa COBOL loanmain.cbl realiza una llamada (CALL) al conector gRPC d8link, esta simula una sentencia EXEC CICS LINK:
- Se define el programa al que se quiere llamar
- El área de intercambio de datos (COMMAREA)
- Y la longitud de la misma
- El conector gRPC d8link recibe los datos (COMMAREA) y llama al microservicio COBOL correspondiente
- El controller gPRC (main.go) gestiona el mensaje proto, lo convierte a una estructura compatible y llama al programa COBOL loancalc.cbl
- El programa COBOL actualiza el área de datos y devuelve el control al controlador gRPC
- Los datos son envíados de vuelta al conector d8link que los copia sobre el área de memoria definida por el programa COBOL
Cree una estructura de directorios como la siguiente:
├── d8link
│ └── link_client
│ └── link_server
│ └── link
│ go.mod
│ go.sum
En el directorio link definiremos nuestro mensaje proto (link.proto)
syntax = "proto3";
option go_package = "github.com/driver8soft/examples/d8link/link";
package link;
// The Link service definition.
service LinkService {
rpc CommArea (CommReq) returns (CommResp) {}
}
// The request message containing program to link, commarea and commarea length.
message CommReq {
string link_prog = 1;
int32 comm_len = 2;
bytes input_msg = 3;
}
// The response message containing commarea
message CommResp {
bytes output_msg = 1;
}
A continuación crearemos el programa d8link.go sobre el directorio link_client
package main
/*
#include <string.h>
#include <stdlib.h>
*/
import "C"
import (
"context"
"flag"
"log"
"unsafe"
pb "github.com/driver8soft/examples/d8link/link"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
var (
addr = flag.String("addr", "localhost:50051", "the address to connect to")
)
//export D8link
func D8link(c_program *C.char, c_commarea *C.char, c_commlen *C.int) C.int {
flag.Parse()
// C variables to Go variables
program := C.GoStringN(c_program, 8) // max length of COBOL mainframe program = 8
commarea := C.GoBytes(unsafe.Pointer(c_commarea), *c_commlen)
commlen := int32(*c_commlen)
log.Println("INFO: Call program -", program)
// Set up a connection to the server.
conn, err := grpc.NewClient(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
client := pb.NewLinkServiceClient(conn)
// Contact the server
r, err := client.CommArea(context.Background(), &pb.CommReq{LinkProg: program, CommLen: commlen, InputMsg: commarea})
if err != nil {
log.Fatalf("ERROR: calling program - %s - %v", program, err)
}
outMsg := r.GetOutputMsg()
C.memcpy(unsafe.Pointer(c_commarea), unsafe.Pointer(&outMsg[0]), C.size_t(commlen))
return 0
}
func main() {
}
Vamos a exportar la función D8link para que pueda ser llamada desde un programa COBOL, para ello es necesario compilarla utilizando la opción c-shared de Go
El compilador de Go generará un objeto (D8link.dylib D8link.so) y un fichero (D8link.h) que serán llamados dinámicamente desde el código COBOL
Y para finalizar crearemos el servidor gRPC (main.go) en el directorio link_server que será el encargado de recibir el mensaje proto y llamar al programa COBOL destino.
package main
/*
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <libcob.h>
#cgo CFLAGS: -I/opt/homebrew/Cellar/gnucobol/3.2/include
#cgo LDFLAGS: -L/opt/homebrew/Cellar/gnucobol/3.2/lib -lcob
static void* allocArgv(int argc) {
return malloc(sizeof(char *) * argc);
}
*/
import "C"
import (
"context"
"flag"
"fmt"
"log"
"net"
"strings"
"time"
"unsafe"
pb "github.com/driver8soft/examples/d8link/link"
"google.golang.org/grpc"
)
var (
port = flag.Int("port", 50051, "The server port")
)
type server struct {
pb.UnimplementedLinkServiceServer
}
func (s *server) CommArea(ctx context.Context, in *pb.CommReq) (out *pb.CommResp, err error) {
start := time.Now()
// remove trailing spaces from program name
program := strings.TrimSpace(in.GetLinkProg())
c_program := C.CString(program)
defer C.free(unsafe.Pointer(c_program))
c_commlen := C.int(in.GetCommLen())
// allocate argc & argv variables
c_argc := C.int(1)
c_argv := (*[0xfff]*C.char)(C.allocArgv(c_argc))
defer C.free(unsafe.Pointer(c_argv))
c_argv[0] = C.CString(string(in.GetInputMsg()))
defer C.free(unsafe.Pointer(c_argv[0]))
// check COBOL program
n := C.cob_resolve(c_program)
if n == nil {
log.Println("ERROR: Module not found. Program name =", program)
} else {
log.Printf("INFO: %s started", program)
ret := C.cob_call(c_program, c_argc, (*unsafe.Pointer)(unsafe.Pointer(c_argv)))
log.Printf("INFO: %s return-code %v", program, ret)
}
c_msg_output := C.GoStringN(c_argv[0], c_commlen)
elapsed := time.Since(start)
log.Printf("INFO: %s elapsed time %s", program, elapsed)
return &pb.CommResp{OutputMsg: []byte(c_msg_output)}, nil
}
func main() {
flag.Parse()
// d8 Initialize gnucobol
C.cob_init(C.int(0), nil)
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatalf("ERROR: failed to listen: %v", err)
}
grpcServer := grpc.NewServer()
pb.RegisterLinkServiceServer(grpcServer, &server{})
log.Printf("INFO: server listening at %v", lis.Addr())
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("ERROR: failed to serve: %v", err)
}
}
Pruebe a realizar llamadas remotas entre programas COBOL intercambiando un área de datos (COPYBOOK). Para ello recuerde que:
- El programa llamador debe compilarse para generar un ejecutable (opción -x GNUCobol)
- El programa llamado debe compilarse para generar un objeto (opción -m GNUCobol)
- Ambos programas deben compilarse utilizando el mismo “byteorder” para compartir datos binarios
- Para simplificar la prueba los programas COBOL puede residir en los directorios definidos anteriormente (link_client link_server)
Puede utilizar los programas COBOL de ejemplo loanmain.cbl y loancalc.cbl.
******************************************************************
*
* Loan Calculator Main Program
* ==========================
*
* A sample program to demonstrate how to create a gRPC COBOL
* microservice.
*
******************************************************************
IDENTIFICATION DIVISION.
PROGRAM-ID. loanmain.
ENVIRONMENT DIVISION.
CONFIGURATION SECTION.
DATA DIVISION.
FILE SECTION.
WORKING-STORAGE SECTION.
* Declare program variables
01 PROG-NAME PIC X(8) VALUE "loancalc".
01 COMMLEN PIC 9(9) COMP.
01 COMMAREA.
05 INPUT-MSG.
10 PRIN-AMT PIC S9(7) USAGE IS DISPLAY.
10 INT-RATE PIC S9(2)V9(2) USAGE IS DISPLAY.
10 TIMEYR PIC S9(2) USAGE IS DISPLAY.
05 OUTPUT-MSG.
10 PAYMENT PIC S9(7)V9(2) USAGE IS DISPLAY.
10 ERROR-MSG PIC X(20).
PROCEDURE DIVISION.
* code goes here!
INITIALIZE COMMAREA.
DISPLAY "Compound Interest Calculator"
DISPLAY "Principal amount: " WITH NO ADVANCING.
ACCEPT PRIN-AMT.
DISPLAY "Interest rate: " WITH NO ADVANCING.
ACCEPT INT-RATE.
DISPLAY "Number of years: " WITH NO ADVANCING.
ACCEPT TIMEYR.
COMPUTE COMMLEN = LENGTH OF COMMAREA.
CALL "D8link" USING PROG-NAME COMMAREA COMMLEN.
DISPLAY "Error Msg: " ERROR-MSG.
DISPLAY "Couta: " PAYMENT.
GOBACK.
******************************************************************
*
* Loan Calculator Subroutine
* ==========================
*
* A sample program to demonstrate how to create a gRPC COBOL
* microservice.
*
******************************************************************
IDENTIFICATION DIVISION.
PROGRAM-ID. loancalc.
ENVIRONMENT DIVISION.
CONFIGURATION SECTION.
DATA DIVISION.
FILE SECTION.
WORKING-STORAGE SECTION.
* Declare program variables
01 WS-MSG.
05 WS-ERROR PIC X(01).
05 WS-MSG00 PIC X(20) VALUE 'OK'.
05 WS-MSG10 PIC X(20) VALUE 'INVALID INT. RATE'.
05 WS-MSG12 PIC X(20) VALUE 'INVALID NUMBER YEARS'.
01 AUX-VARS.
05 MONTHLY-RATE USAGE IS COMP-2.
05 AUX-X USAGE IS COMP-2.
05 AUX-Y USAGE IS COMP-2.
05 AUX-Z USAGE IS COMP-2.
LINKAGE SECTION.
* Data to share with COBOL subroutines
01 LOAN-PARAMS.
05 INPUT-MSG.
10 PRIN-AMT PIC S9(7) USAGE IS DISPLAY.
10 INT-RATE PIC S9(2)V9(2) USAGE IS DISPLAY.
10 TIMEYR PIC S9(2) USAGE IS DISPLAY.
05 OUTPUT-MSG.
10 PAYMENT PIC S9(7)V9(2) USAGE IS DISPLAY.
10 ERROR-MSG PIC X(20).
PROCEDURE DIVISION USING BY REFERENCE LOAN-PARAMS.
* code goes here!
000-MAIN.
MOVE "N" TO WS-ERROR.
DISPLAY "PRIN-AMT: " PRIN-AMT.
DISPLAY "INT-RATE: " INT-RATE.
DISPLAY "TIMEYR: " TIMEYR.
PERFORM 100-INIT.
IF WS-ERROR = 'N'
PERFORM 200-PROCESS
END-IF.
PERFORM 300-WRAPUP.
100-INIT.
IF INT-RATE <= 0
MOVE WS-MSG10 TO ERROR-MSG
MOVE 10 TO RETURN-CODE
MOVE 'Y' TO WS-ERROR
ELSE
IF TIMEYR <= 0
MOVE WS-MSG12 TO ERROR-MSG
MOVE 12 TO RETURN-CODE
MOVE 'Y' TO WS-ERROR
END-IF
END-IF.
200-PROCESS.
INITIALIZE AUX-VARS.
COMPUTE MONTHLY-RATE = (INT-RATE / 12 / 100).
COMPUTE AUX-X = ((1 + MONTHLY-RATE) ** (TIMEYR*12)).
COMPUTE AUX-Y = AUX-X * MONTHLY-RATE.
COMPUTE AUX-Z = (AUX-X - 1) / AUX-Y.
COMPUTE PAYMENT = PRIN-AMT / AUX-Z.
MOVE WS-MSG00 TO ERROR-MSG.
MOVE 0 TO RETURN-CODE.
DISPLAY "PAYMENT: " PAYMENT.
DISPLAY "ERROR-MSG: " ERROR-MSG.
300-WRAPUP.
GOBACK.