Interoperability with C, Java and Python

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against ISO/IEC 1989:2023 (edition 3, January 2023; an upcoming 4th edition is in ballot), whose CALL statement, BY REFERENCE, BY CONTENT and BY VALUE phrases and RETURNING phrase come from COBOL 85 and 2002 (the standard says nothing about C, Java or Python, so everything language-specific below belongs to the products); against IBM Enterprise COBOL for z/OS 6.5 (PTFs through September 2026) on z/OS 3.2, whose Language Environment (LE) supports calls between COBOL and C, C++, PL/I and assembler and whose 6.4 and 6.5 releases add a non-object-oriented (non-OO) Java interoperability framework; and against GnuCOBOL 3.2 (28 July 2023, the latest release), with which every portable program on it was compiled (cobc -x -Wall for the COBOL, gcc -std=c11 -Wall -Wextra for the C) and run on 2026-10-20 in a Debian container (GCC 14.2, aarch64, Python 3.13). z/OS examples are labelled "z/OS; not run here" and are checked against the Programming Guide, the Language Reference and the Language Environment interlanguage guide.

COBOL rarely lives alone. The Circulation back office keeps its fine and ISBN rules in COBOL, but a branch’s self-service kiosk is written in C, the catalogue web application is Java on the mainframe, and a data team wants to call the same fine rule from a Python script. This page shows how the languages meet. It covers GnuCOBOL calling C, C calling COBOL, Python’s ctypes loading a GnuCOBOL module, the data layout both sides must agree on, then the z/OS side: LE inter-language calls and the Java framework. Plain COBOL-to-COBOL calls, LINKAGE and the passing modes are on Subprograms CALL and Linkage; addresses and USAGE POINTER are on Pointers and Dynamic Storage; the byte layout of each USAGE is on Usage and Numeric Representation. The C, Java and Python languages themselves are not re-explained: see the C reference starting at Getting Started ©, the Java reference starting at Getting Started (Java) and the Python reference starting at Getting Started (Python).

Where the book is outdated: Taulli’s Modern Mainframe Development (2022, chapter 7) discusses Java, C/C++, PL/I and assembler next to COBOL as a survey, written for Enterprise COBOL 6.3, which it calls the latest release. That predates the non-OO Java interoperability of 6.4 (JAVA-CALLABLE, JAVA-SHAREABLE, CALL of a static Java method, JAVAIOP, cjbuild) and its 6.5 refinements described below, so the object-oriented route through INVOKE that older texts present as the way to reach Java is no longer the only one, and for most uses no longer the simplest. The book is also centered on the mainframe; GnuCOBOL on a PC or Linux, where C is the native tongue, is covered here from the manuals and from running the programs.

The picture

Every combination below passes through the same two facts. First, COBOL passes the address of its data (BY REFERENCE, the default) and C receives a pointer: there is no copy and no trailing NUL byte. Second, a COBOL program needs the COBOL run time initialized (libcob in GnuCOBOL, Language Environment on z/OS) before it runs, so a program that is not itself started by the COBOL run time has to start it first.

flowchart LR A["COBOL main program, built with cobc -x"] -->|"CALL by name"| B["C function"] C["C main program"] -->|"cob_init, then call"| D["COBOL module"] E["Python with ctypes"] -->|"cob_init, then call"| D A --> F[("libcob run time")] D --> F

What the two sides see: bytes

COBOL data is described by its PICTURE and USAGE, C data by its type. The bytes are the contract, and only some USAGE choices have a C equivalent. The next program passes five items to a C function that prints their bytes, so the layout is visible instead of assumed. BY VALUE 4 is used here only as a length argument: it is a numeric literal passed by value, and the C function receives it as the count of bytes to show.

/* bytes.c - print n bytes at an address */
#include <stdio.h>

void dump(const unsigned char *p, int n)
{
    for (int i = 0; i < n; i++) {
        printf("%02X ", p[i]);
    }
    printf("\n");
}
       IDENTIFICATION DIVISION.
       PROGRAM-ID. BYTES.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  A-COMP             PIC S9(9) COMP VALUE 1.
       01  A-COMP5            PIC S9(9) COMP-5 VALUE 1.
       01  A-LONG             BINARY-LONG VALUE 1.
       01  A-DISP             PIC 9(5)V99 VALUE 5.
       01  A-PACKED           PIC S9(5)V99 COMP-3 VALUE -5.25.
       PROCEDURE DIVISION.
           CALL "dump" USING A-COMP BY VALUE 4
           CALL "dump" USING A-COMP5 BY VALUE 4
           CALL "dump" USING A-LONG BY VALUE 4
           CALL "dump" USING A-DISP BY VALUE 7
           CALL "dump" USING A-PACKED BY VALUE 4
           STOP RUN.

Compiled with cobc -x -Wall bytes.cob bytes.c (and with -std=ibm), no warnings, it prints:

00 00 00 01
01 00 00 00
01 00 00 00
30 30 30 30 35 30 30
00 00 52 5D

Reading the lines in order:

  • PIC S9(9) COMP is stored big-endian (00 00 00 01), as on the mainframe, regardless of the machine. A C int on this machine is little-endian, so C cannot read it directly. GnuCOBOL’s binary-byteorder setting controls this, and COMP is the form that matches z/OS data files.

  • PIC S9(9) COMP-5 and BINARY-LONG are the machine’s native order (01 00 00 00 here), the same as a C int. BINARY-LONG is the native 32-bit integer by definition; COMP-5 is the native form that keeps the digit count of the PICTURE.

  • A DISPLAY number is one character per digit, with the decimal point implied: 5.00 in PIC 9(5)V99 is the seven bytes 30 30 30 30 35 30 30, the ASCII characters 0000500. It is a string of digits, not an integer.

  • A packed number (COMP-3) holds two digits per byte with the sign in the last half-byte: -5.25 is 00 00 52 5D, the digits 0000525 and the sign D (negative). C has no native type for it.

The consequence is a simple rule for C and Python interfaces: pass BINARY-LONG (or BINARY-SHORT, BINARY-DOUBLE, COMP-1/COMP-2 floating point) for numbers and PIC X(n) for text, and decode DISPLAY and packed items explicitly when they cannot be avoided.

COBOL (GnuCOBOL) C on a 64-bit Linux system Notes

BINARY-CHAR, BINARY-SHORT, BINARY-LONG, BINARY-DOUBLE

signed char, short, int, long long

Native byte order, so they match without conversion.

BINARY-C-LONG

long

64 bits on 64-bit Linux, 32 bits on 64-bit Windows: it follows the C compiler.

PIC S9(9) COMP-5

int

Native order; the digit count selects the size.

COMP-1, COMP-2

float, double

IEEE binary floating point in GnuCOBOL. On z/OS the same names are IBM hexadecimal floating point by default (see below).

PIC X(n)

char[n]

Not NUL-terminated and padded with spaces: add the terminator or pass the length.

PIC 9(n)V99 (DISPLAY)

char[n+2] of ASCII digits

The decimal point is not stored.

PIC S9(n) COMP, COMP-3

none

Big-endian binary and packed decimal: decode by hand.

USAGE POINTER

void *

A C function can return an address that COBOL takes with SET ADDRESS OF.

GnuCOBOL calling C

A GnuCOBOL program calls a C function exactly as it calls a COBOL program: CALL "name". cobc compiles .c files too, so a single command builds both. The C file below holds two Circulation rules: the ISBN-13 check digit and the overdue fine (0.35 a day, capped at 5.00, as on Arithmetic and COMPUTE).

/* circ_c.c - small C helpers called from the Circulation COBOL programs */
#include <stddef.h>

/* ISBN-13 check digit: returns 1 when the 13 digits are valid, else 0. */
int isbn13_valid(const char *isbn)
{
    int sum = 0;
    for (size_t i = 0; i < 13; i++) {
        if (isbn[i] < '0' || isbn[i] > '9') {
            return 0;
        }
        sum += (isbn[i] - '0') * ((i % 2 == 0) ? 1 : 3);
    }
    return sum % 10 == 0;
}

/* Overdue fine in cents: 35 cents a day, capped at 500. */
int fine_cents(int days)
{
    int cents = days * 35;
    return cents > 500 ? 500 : cents;
}
      * CALLC - COBOL calling C: BY REFERENCE, BY VALUE and RETURNING
       IDENTIFICATION DIVISION.
       PROGRAM-ID. CALLC.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       COPY BOOKREC.
       01  WS-DAYS            BINARY-LONG.
       01  WS-CENTS           BINARY-LONG.
       01  WS-VALID           BINARY-LONG.
       01  WS-ISBN-Z          PIC X(14).
       01  WS-SHOW            PIC Z(4)9.
       PROCEDURE DIVISION.
       MAIN-PARA.
           MOVE "9780000000011" TO BOOK-ISBN
           PERFORM CHECK-ISBN
           MOVE "9780000000019" TO BOOK-ISBN
           PERFORM CHECK-ISBN
           MOVE 20 TO WS-DAYS
           PERFORM FINE-FOR-DAYS
           MOVE 3 TO WS-DAYS
           PERFORM FINE-FOR-DAYS
           STOP RUN.

       CHECK-ISBN.
      *    C strings end in a NUL byte, so build a terminated copy.
           MOVE LOW-VALUES TO WS-ISBN-Z
           MOVE BOOK-ISBN TO WS-ISBN-Z(1:13)
           CALL "isbn13_valid" USING BY REFERENCE WS-ISBN-Z
                RETURNING WS-VALID
           MOVE WS-VALID TO WS-SHOW
           DISPLAY "ISBN " BOOK-ISBN " valid flag " WS-SHOW.

       FINE-FOR-DAYS.
           CALL "fine_cents" USING BY VALUE WS-DAYS
                RETURNING WS-CENTS
           MOVE WS-DAYS TO WS-SHOW
           DISPLAY WS-SHOW " days -> " WITH NO ADVANCING
           MOVE WS-CENTS TO WS-SHOW
           DISPLAY WS-SHOW " cents".

Compiled with cobc -x -Wall -I. callc.cob circ_c.c (the copybook BOOKREC.cpy is in the same directory; -fstatic-call and -std=ibm give identical results), no warnings, it prints:

ISBN 9780000000011 valid flag     0
ISBN 9780000000019 valid flag     1
   20 days ->   500 cents
    3 days ->   105 cents

The first ISBN is the master file’s placeholder value, whose check digit is wrong (the correct last digit is 9); the second differs only in that digit. The fine for 20 days is capped at 5.00, and for 3 days it is 1.05, the same results as the COBOL calculation. The pieces to notice:

  • BY REFERENCE hands C a pointer to WS-ISBN-Z. C reads exactly 13 characters here, so the NUL byte is not strictly needed, but a general C string function would run off the end without it. COBOL fields are space-padded, never NUL-terminated.

  • BY VALUE WS-DAYS passes the number itself, not an address. RETURNING WS-CENTS stores the C function’s int result into a BINARY-LONG.

  • The C function names are case-sensitive and must match the literal in the CALL; a COBOL program name, by contrast, is the PROGRAM-ID as written.

The C file passes gcc -std=c11 -Wall -Wextra -pedantic without warnings.

Static and dynamic calls

Whether CALL "literal" is resolved when the program is linked or when it runs is a compile-time choice. By default cobc generates a run-time lookup, so a CALL can reach a program in another file that is loaded on demand (Subprograms CALL and Linkage). With -fstatic-call (the same as -static) it generates a direct C call that the linker resolves. The generated C shows the difference (cobc -C writes it out):

dynamic (default):
  call_fine_cents.funcvoid = cob_resolve_cobol ("fine_cents", 0, 1);
  ret = ((int (*)(cob_u64_t))call_fine_cents.funcint) ((cob_u64_t)((*(int *)(b_27))));

with -fstatic-call:
  ret = fine_cents ((cob_u64_t)((*(int *)(b_27))));

Both builds above worked because the C file was given to cobc on the command line and became part of the executable. When it is not, the two kinds of call fail differently, and the messages are real:

dynamic call, C file not linked (at run time):
libcob: error: module 'isbn13_valid' not found

-fstatic-call, C file not linked (at link time):
/usr/bin/ld: /tmp/cob19_0.o: in function `CALLC_':
cob19_0.c:(.text+0x30c): undefined reference to `isbn13_valid'
/usr/bin/ld: cob19_0.c:(.text+0x3b8): undefined reference to `fine_cents'
collect2: error: ld returned 1 exit status

The static form finds a missing function before the program ever runs; the manual lists that all called programs are verified to be present, and a slightly faster call, as the benefits of static linking. The dynamic form is the one that allows a C file built separately as a module. cobc -m -Wall circ_c.c produced circ_c.so, and the same executable ran with COB_LIBRARY_PATH=. only when the module had been loaded first:

COB_LIBRARY_PATH=. ./callc
libcob: error: module 'isbn13_valid' not found

COB_LIBRARY_PATH=. COB_PRE_LOAD=circ_c ./callc
ISBN 9780000000011 valid flag     0
ISBN 9780000000019 valid flag     1
   20 days ->   500 cents
    3 days ->   105 cents

The run-time search finds a module by the file name of the shared object, which here is circ_c.so and not the name of any function inside it. That is the reason for the manual’s remark that the binary must be named like the CALL target or have been loaded before; COB_PRE_LOAD loads it up front. A C module that contains several functions should therefore be preloaded, or linked into the executable, instead of being found by function name.

BY VALUE and the size of the argument

BY VALUE is a COBOL 2002 phrase meant for exactly this: the argument is a number placed on the call stack or in a register, not an address. GnuCOBOL 3.2 changed one detail that matters whenever both sides are compiled by different versions. The release notes list it under "Changes that potentially effect recompilation of existing programs": in 64-bit environments, a BY VALUE parameter without an explicit SIZE IS clause used to be passed as a 32-bit value and is now passed as a 64-bit value; to pass 32 bits, write SIZE IS 4, and to keep relying on the default, caller and callee must both be compiled with a version before 3.2, or both since. The following program shows what each C parameter type receives. The values are chosen to expose sign and width.

#include <stdio.h>
void show_short(short v)        { printf("short     %d\n", v); }
void show_int(int v)            { printf("int       %d\n", v); }
void show_long(long v)          { printf("long      %ld\n", v); }
void show_longlong(long long v) { printf("longlong  %lld\n", v); }
void show_double(double v)      { printf("double    %.2f\n", v); }
      * SIZES - how wide is a BY VALUE argument?
       IDENTIFICATION DIVISION.
       PROGRAM-ID. SIZES.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  V-SHORT            BINARY-SHORT VALUE -5.
       01  V-LONG             BINARY-LONG VALUE -70000.
       01  V-DOUBLE-INT       BINARY-DOUBLE VALUE -5000000000.
       01  V-C-LONG           BINARY-C-LONG VALUE -5000000000.
       01  V-FLOAT            COMP-2 VALUE 12.75.
       PROCEDURE DIVISION.
           CALL "show_short" USING BY VALUE V-SHORT
           CALL "show_int" USING BY VALUE V-LONG
           CALL "show_long" USING BY VALUE V-C-LONG
           CALL "show_longlong" USING BY VALUE V-DOUBLE-INT
           CALL "show_double" USING BY VALUE V-FLOAT
           STOP RUN.

Compiled with cobc -x -Wall sizes.cob sizes.c, no warnings, it prints:

short     -5
int       -70000
long      -5000000000
longlong  -5000000000
double    12.75

The generated C shows how: every integer BY VALUE argument is widened (with its sign) to a 64-bit cob_u64_t before the call, and a floating-point one is passed as a double. On the 64-bit calling conventions used on Linux, a C function that declares short or int reads the low bits of the register and gets the right value, so the widening is harmless to C. It matters when the callee is itself a GnuCOBOL program compiled with PROCEDURE DIVISION USING BY VALUE, because the two sides must agree on the width, and it matters on a calling convention where arguments are laid out on the stack by size. To ask for a 32-bit argument explicitly, put SIZE IS 4 before the item (the phrase is written ahead of the identifier):

      * SIZE4 - explicit SIZE IS 4 on a BY VALUE argument
       IDENTIFICATION DIVISION.
       PROGRAM-ID. SIZE4.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  V-LONG             BINARY-LONG VALUE -70000.
       PROCEDURE DIVISION.
           CALL "show_int" USING BY VALUE SIZE IS 4 V-LONG
                RETURNING NOTHING
           END-CALL
           STOP RUN.

Compiled with cobc -x -Wall size4.cob sizes.c, no warnings, it prints int -70000, and the generated call now passes a cob_u32_t to a function declared void (*)(cob_u32_t). RETURNING NOTHING tells cobc that the C function is void, so no result is expected (another 3.2 addition for calling C directly, along with CALL-CONVENTION and ENTRY-CONVENTION).

Source: GnuCOBOL 3.2 manual: CALL; the GnuCOBOL 3.2 NEWS file, section "Changes that potentially effect recompilation of existing programs", in the source distribution at ftp.gnu.org/gnu/gnucobol

Passing text and getting text back

BY VALUE is for numbers. Naming an alphanumeric item there is a mistake that cobc -Wall catches, because C cannot receive a COBOL string by value:

           MOVE "W" TO BOOK-STATUS
           CALL "status_text" USING BY VALUE BOOK-STATUS
                RETURNING WS-PTR
badstatus.cob: in paragraph 'MAIN-PARA':
badstatus.cob:14: warning: BY CONTENT assumed for alphanumeric item 'BOOK-STATUS' [-Wothers]
   14 >            CALL "status_text" USING BY VALUE BOOK-STATUS

cobc silently changes the call into BY CONTENT, which passes the address of a temporary copy, and the C function status_text(char code) then received that address as if it were the character. The program printed status W is Unknown, a wrong answer with only a warning to show for it. The correct form passes the item by reference to a function that takes a pointer, and takes the returned const char * into a USAGE POINTER, then overlays a LINKAGE item on it with SET ADDRESS OF (the technique of Pointers and Dynamic Storage):

/* Text for a BOOK-STATUS code; the pointer stays valid (static storage). */
const char *status_text(char code)
{
    switch (code) {
    case 'A': return "Active";
    case 'W': return "Withdrawn";
    default:  return "Unknown";
    }
}

/* Same lookup, but the code arrives by reference as COBOL passes an item. */
const char *status_text_ref(const char *code)
{
    return status_text(*code);
}
      * CALLC2 - a C function that returns a char pointer
       IDENTIFICATION DIVISION.
       PROGRAM-ID. CALLC2.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       COPY BOOKREC.
       01  WS-PTR             USAGE POINTER.
       01  WS-TEXT            PIC X(40).
       LINKAGE SECTION.
       01  LK-TEXT            PIC X(40).
       PROCEDURE DIVISION.
       MAIN-PARA.
           MOVE "W" TO BOOK-STATUS
           CALL "status_text_ref" USING BY REFERENCE BOOK-STATUS
                RETURNING WS-PTR
           SET ADDRESS OF LK-TEXT TO WS-PTR
           MOVE SPACES TO WS-TEXT
           STRING LK-TEXT DELIMITED BY LOW-VALUE INTO WS-TEXT
           DISPLAY "status " BOOK-STATUS " is " FUNCTION TRIM(WS-TEXT)
           STOP RUN.

Compiled with cobc -x -Wall -I. callc2.cob circ_c.c (and with -std=ibm), no warnings, it prints:

status W is Withdrawn

STRING ... DELIMITED BY LOW-VALUE copies up to the NUL byte that ends the C string, which also keeps the program from using the 40 bytes of LK-TEXT beyond the end of the literal. The address belongs to C: the COBOL program must not FREE it, and the C function must return storage that outlives the call (a literal, a static buffer, or memory the C side agrees to release later).

C calling COBOL

The reverse direction needs the C program to start the COBOL run time before the first call. The manual’s rule is: include libcob.h, call cob_init, then call the COBOL modules, then clean up with cob_stop_run if the program should end, or with cob_tidy if it should keep running without further COBOL calls. The module below is the fine rule from the previous section written in COBOL, with its result in a DISPLAY field and a return code.

      * FINECALC - overdue fine: 0.35 a day, capped at 5.00
       IDENTIFICATION DIVISION.
       PROGRAM-ID. FINECALC.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-CALLS           BINARY-LONG VALUE 0.
       LINKAGE SECTION.
       01  LK-DAYS            BINARY-LONG.
       01  LK-FINE            PIC 9(3)V99.
       PROCEDURE DIVISION USING LK-DAYS LK-FINE.
       MAIN-PARA.
           ADD 1 TO WS-CALLS
           IF LK-DAYS < 0
               MOVE 0 TO LK-FINE
               MOVE 8 TO RETURN-CODE
               GOBACK
           END-IF
           COMPUTE LK-FINE = LK-DAYS * 0.35
               ON SIZE ERROR MOVE 5.00 TO LK-FINE
           END-COMPUTE
           IF LK-FINE > 5.00
               MOVE 5.00 TO LK-FINE
           END-IF
           MOVE 0 TO RETURN-CODE
           GOBACK.
/* driver.c - a C main program that calls the COBOL module FINECALC */
#include <stdio.h>
#include <string.h>
#include <libcob.h>

int main(void)
{
    int days[] = { 3, 20, -1 };

    cob_init(0, NULL);                      /* start the COBOL run time */

    for (size_t i = 0; i < sizeof days / sizeof days[0]; i++) {
        char fine[5];                       /* PIC 9(3)V99: five digit bytes */
        void *argv[2] = { &days[i], fine };
        int rc = cob_call("FINECALC", 2, argv);
        printf("days=%3d  fine=%.3s.%.2s  return code=%d\n",
               days[i], fine, fine + 3, rc);
    }

    cob_stop_run(0);                        /* flush, close files, exit */
}

The module was built as a shared library with cobc -m -Wall finecalc.cob (warning-free), the driver with gcc -std=c11 -Wall -Wextra -pedantic driver.c -lcob (also warning-free), and run with COB_LIBRARY_PATH pointing at the module’s directory. It prints:

days=  3  fine=001.05  return code=0
days= 20  fine=005.00  return code=0
days= -1  fine=000.00  return code=8

cob_call(name, argc, argv) finds the module (by the same search that CALL uses), passes the argv entries as BY REFERENCE arguments in order, and returns the module’s RETURN-CODE. Run from a directory that the module search does not cover, the same driver stops with libcob: error: module 'FINECALC' not found. The simplest build avoids the search entirely: cobc -x -Wall driver.c finecalc.cob compiles both, links them into one executable and prints the same three lines.

Calling the entry point directly

A COBOL program compiled for static use is an ordinary C function whose name is the PROGRAM-ID and whose parameters are the USING items, each a pointer. The manual shows the prototype extern int say(char *hello, char *world) for a module with two PIC X parameters; here it is for FINECALC:

/* direct.c - call the COBOL entry point as an ordinary C function */
#include <stdio.h>
#include <libcob.h>

extern int FINECALC(int *days, char *fine);

int main(void)
{
    int days = 7;
    char fine[5];

#ifndef SKIP_INIT
    cob_init(0, NULL);
#endif
    int rc = FINECALC(&days, fine);
    printf("days=%d fine=%.3s.%.2s rc=%d\n", days, fine, fine + 3, rc);
    cob_stop_run(0);
}

Built with cobc -c -Wall finecalc.cob -o finecalc.o and gcc -std=c11 -Wall -Wextra -pedantic direct.c finecalc.o -lcob, it prints:

days=7 fine=002.45 rc=0

Built again with -DSKIP_INIT, so that cob_init is not called, the same program fails at its first COBOL statement with the run time’s own message and exit status 1:

libcob: error: cob_init() has not been called

That check is why cob_init comes first in every C program above. A caller that wants to choose a module by name at run time, and to report a missing one instead of stopping, uses cob_resolve, which returns a function pointer or NULL, together with cob_resolve_error for the text:

/* resolve.c - look a COBOL module up by name before calling it */
#include <stdio.h>
#include <libcob.h>

typedef int (*finecalc_fn)(int *days, char *fine);

int main(void)
{
    cob_init(0, NULL);

    finecalc_fn finecalc = (finecalc_fn)cob_resolve("FINECALC");
    if (finecalc == NULL) {
        fprintf(stderr, "cannot load FINECALC: %s\n", cob_resolve_error());
        cob_stop_run(1);
    }

    int days = 12;
    char fine[5];
    int rc = finecalc(&days, fine);
    printf("days=%d fine=%.3s.%.2s rc=%d\n", days, fine, fine + 3, rc);

    cob_tidy();                 /* more C code could follow here */
    return rc;
}

Compiled with gcc -Wall -Wextra resolve.c -lcob (the conversion of the object pointer returned by cob_resolve to a function pointer is what -pedantic objects to, as in the manual’s own example), it prints days=12 fine=004.20 rc=0 when FINECALC.so is on COB_LIBRARY_PATH, and cannot load FINECALC: module 'FINECALC' not found (exit status 1) when it is not. cob_tidy shuts the COBOL run time down but lets the C program carry on, while cob_stop_run ends the process; the manual points out that errors inside COBOL end the program like STOP RUN does, and describes cob_call_with_exception_check for callers that must regain control.

Python with ctypes

Python’s standard ctypes module loads shared libraries and calls their functions, which is all a GnuCOBOL module is. The script loads libcob first, starts the run time with cob_init, loads the module, declares its argument types and calls it. The Circulation scenario is the same fine rule, and the DISPLAY result is decoded into a Decimal, because the five digit bytes are text and not a number.

"""fines.py - load the GnuCOBOL module FINECALC with ctypes."""
import ctypes
import ctypes.util
import sys
from decimal import Decimal

libcob = ctypes.CDLL(ctypes.util.find_library("cob") or "libcob.so",
                     mode=ctypes.RTLD_GLOBAL)

if "--no-init" not in sys.argv:
    libcob.cob_init(0, None)            # start the COBOL run time first

module = ctypes.CDLL("./FINECALC.so")
finecalc = module.FINECALC
finecalc.argtypes = [ctypes.POINTER(ctypes.c_int32), ctypes.c_char_p]
finecalc.restype = ctypes.c_int

for days in (3, 20, -1):
    fine = ctypes.create_string_buffer(5)       # PIC 9(3)V99: five digit bytes
    rc = finecalc(ctypes.byref(ctypes.c_int32(days)), fine)
    text = fine.raw.decode("ascii")
    value = Decimal(text[:3] + "." + text[3:])
    print(f"days={days:3d}  raw={text!r}  fine={value}  return code={rc}")

libcob.cob_tidy()                       # close COBOL files, free run-time storage

With the module built by cobc -m -Wall finecalc.cob in the current directory and Python 3.13 installed in the container, python3 fines.py prints:

days=  3  raw='00105'  fine=1.05  return code=0
days= 20  raw='00500'  fine=5.00  return code=0
days= -1  raw='00000'  fine=0.00  return code=8

The points that mattered when running it:

  • The LINKAGE item LK-DAYS is BINARY-LONG, a native 32-bit integer, so c_int32 is the matching ctypes type; a PIC 9(3)V99 result is five characters, so a five-byte buffer is passed. A COMP item would arrive big-endian and need int.from_bytes(..., "big").

  • Without cob_init, the script ends at the first call with libcob: error: cob_init() has not been called and exit status 1, the same message the C program printed.

  • Ending the script with libcob.cob_stop_run(0) instead of cob_tidy loses the output when it is piped or redirected. cob_stop_run calls the C library’s exit, which skips Python’s own output buffer: python3 fines_stoprun.py | head and python3 fines_stoprun.py > out.txt both produced no output at all, although the exit status was 0, while the same script with cob_tidy (or with python3 -u) printed all three lines. A Python caller should use cob_tidy, or flush sys.stdout first.

For numbers and short text this is enough. A module that opens files, holds state in WORKING-STORAGE between calls or sorts keeps its state inside the one Python process that loaded it, and the first-call/CANCEL rules of Subprograms CALL and Linkage apply to it as to any other caller. Python’s Decimal, which is the natural counterpart of PIC 9V99 data, is covered at Numbers (Python); the Python language itself is at Getting Started (Python).

z/OS: Language Environment inter-language calls

On z/OS, COBOL, C, C, PL/I and assembler programs that follow the Language Environment conventions share one run-time environment, and so can call each other. The rules are in the Language Environment guide for writing interlanguage communication (ILC) applications, whose chapters cover C and COBOL, C and COBOL, COBOL and PL/I, COBOL and Fortran, and calling COBOL from assembler. The guide is reproduced here only in summary and with original examples, labelled "z/OS; not run here". The environment is described on Compiling, Linking and Language Environment and Mainframe Environment Primer.

Declaring the call

There are two ways to declare a C function so that COBOL can call it, and two for the other direction. The form without a pragma needs no special declaration: the arguments travel as C passes them, so COBOL names them BY VALUE.

      * z/OS; not run here. COBOL calling C without #pragma linkage
       01  WS-DAYS            PIC S9(9) BINARY.
           CALL "FINECENT" USING BY VALUE WS-DAYS.
/* z/OS; not run here. C function called from COBOL */
void FINECENT(int days)
{
    /* ... */
}

The second form marks the C function with #pragma linkage(name,COBOL), which makes it use the COBOL convention, so the arguments arrive by reference, as they do between COBOL programs:

      * z/OS; not run here. COBOL calling C with #pragma linkage
       01  WS-DAYS            PIC S9(9) BINARY.
           CALL "FINECENT" USING BY REFERENCE WS-DAYS.
/* z/OS; not run here. */
#pragma linkage(FINECENT,COBOL)
void FINECENT(int *days)
{
    /* ... */
}

In the other direction, a C program that calls the COBOL program FINECALC declares void FINECALC(int) and passes the value when the COBOL program says PROCEDURE DIVISION USING BY VALUE LK-DAYS, or declares #pragma linkage(FINECALC,COBOL) and passes the address when the COBOL program uses plain USING LK-DAYS. The pairs above are the patterns of the interlanguage guide, written with Circulation names.

What is supported

The guide gives tables of which combinations work. The ones that matter in practice:

  • Static and dynamic calls. Enterprise COBOL compiled with NODLL can call C by a static or a dynamic CALL, with or without the pragma, provided the C code does not export functions or variables from a DLL when the call is dynamic. COBOL compiled with DLL can call a function in its own module, or a function that a reentrant C DLL exports.

  • Fetch. C reaches a COBOL program by a static call, or by the C fetch() library function (a fetched COBOL load module must not itself contain DLL routines that export functions or variables). A COBOL CANCEL cannot be used on a program that was loaded by fetch(); C uses release().

  • XPLINK. C compiled with XPLINK is automatically DLL-enabled, and an XPLINK C routine must reside in a separate module from the COBOL caller.

  • Older compilers. VS COBOL II and COBOL/370 need the pragma, and need a void function in either direction. Enterprise COBOL does not.

  • Speed. The guide notes that COBOL calling C is fastest without #pragma linkage(...,COBOL).

The guide also covers data-type equivalents (a one-byte char against PIC X, short against PIC S9(4) BINARY, int against PIC S9(9) BINARY, and so on), strings, condition handling when an exception occurs in COBOL (including CEEMRCR) and output ordering when COBOL is the main routine and C writes to the same stream. Under CICS, the Programming Guide says that ILC with other high-level languages is supported, refers to the ILC guide for the rules, and names CICS LINK, XCTL and RETURN as the alternative where ILC is not supported.

DLLs are a third route between C and COBOL. The Programming Guide states that COBOL DLL support interoperates with the DLL support of the z/OS C/C products, except for COBOL `EXTERNAL` data: COBOL can call functions exported from C/C DLLs, and C/C++ can call COBOL programs that a COBOL DLL exports. A C function can also return a function pointer that COBOL keeps in a FUNCTION-POINTER item (CALL "c-function" RETURNING fp, then CALL fp), the case discussed with the pointer features on Pointers and Dynamic Storage.

z/OS: calling Java and being called by Java

Enterprise COBOL had an object-oriented route to Java for many years: write the COBOL as a class, INVOKE Java methods, and use the Java Native Interface (JNI) services through the JNI.cpy copybook and the JNIEnvPtr special register. That framework is still there and is described on Object Oriented COBOL. Enterprise COBOL 6.4 added a second one that needs no classes: an existing procedural program can be made callable from Java with one directive, and can call a static Java method with an ordinary CALL. The 6.5 Programming Guide presents it as a more comprehensive and more flexible form of interoperability than the OO framework, and gives it its own part (Part 7, chapters 43 to 48), next to chapter 18 on building it. Its pieces:

  • The JAVA-CALLABLE directive makes a COBOL program a native method of a Java class.

  • The JAVA-SHAREABLE directive exposes WORKING-STORAGE items to Java for reading and writing.

  • A CALL whose target is a literal of the form 'Java.class-name.static-method-name' calls a static Java method.

  • The JAVAIOP compiler option controls where the compiler writes the stub files and which Java the program targets.

  • The cjbuild utility turns the stub files into the DLL that the JVM and the COBOL programs need at run time.

Everything in this section is described from the IBM manuals and is "z/OS; not run here": the compiler, cjbuild and a JVM on z/OS are not available outside the platform.

Making a program callable from Java

The directive is written >>JAVA-CALLABLE anywhere before the PROCEDURE DIVISION header of the outermost program of the compilation unit. The program must have a well-defined interface of Java-compatible parameters and an optional RETURNING item, and its name is at most eight characters long, because the stub calls it dynamically. The Circulation fine rule becomes:

      * z/OS; not run here. FINECALC callable from Java
       IDENTIFICATION DIVISION.
       PROGRAM-ID. FINECALC.
       DATA DIVISION.
       LINKAGE SECTION.
       01  LK-DAYS            PIC S9(9) COMP-5.
       01  LK-CENTS           PIC S9(9) COMP-5.
       >>JAVA-CALLABLE
       PROCEDURE DIVISION USING LK-DAYS RETURNING LK-CENTS.
       MAIN-PARA.
           IF LK-DAYS < 0
               MOVE 0 TO LK-CENTS
           ELSE
               COMPUTE LK-CENTS = LK-DAYS * 35
               IF LK-CENTS > 500
                   MOVE 500 TO LK-CENTS
               END-IF
           END-IF
           GOBACK.

The four build steps are those of Programming Guide chapter 18 (compile, build the stub DLL, link, compile the Java). The commands below follow the guide’s cob2 and cjbuild forms; the directory and library names are placeholders:

# 1. Compile; stubs go to the OUTPATH directory, which must already exist
cob2 -c finecalc.cbl "-qjavaiop(OUTPATH('${HOME}/circ/stubs'))"

# 2. Build the stub DLL and its side deck, and the Java classes
cjbuild -v -c ${HOME}/circ/stubs -j ${HOME}/circ/classes -p circulation.cobol circ1

# 3. Link the program with the side deck
cob2 finecalc.o -o "//USERID.CIRC.LOAD(FINECALC)" libcirc1.x

# 4. Compile the Java program, then run it
javac -cp ${HOME}/circ/classes CircJava.java
java -Djava.library.path=${HOME}/circ -cp ${HOME}/circ/classes:. CircJava
// z/OS; not run here
import circulation.cobol.*;

public class CircJava {
    public static void main(String[] args) {
        int cents = progs.FINECALC(20);
        System.out.println("20 days -> " + cents + " cents");
    }
}

USERID.CIRC.LOAD must be in the STEPLIB when the program runs, because the generated stub finds FINECALC by a dynamic call, and the Java program finds the DLL through java.library.path (for a DLL on the z/OS UNIX file system) or the STEPLIB (for a data set). The stub’s Java signature is generated from the COBOL definition, so the Java compiler checks the argument types at compile time. The class is called progs in the package named by the -p option of cjbuild (enterprise.COBOL when the option is omitted), and the name of the method is the upper-cased program name, unless the program name is written as a literal or compiled with PGMNAME(LONGMIXED), in which case the case is kept exactly.

The type mapping

The compiler decides how to convert each parameter from its definition. The Java-compatible elementary types are these (the Language Reference gives the full tables, including the array types):

COBOL Java

PIC X

byte

PIC X(m) (m greater than 1), PIC N(m) NATIONAL, PIC U(m)

String

PIC S9(n) COMP-5, n 1 to 4

short

PIC S9(n) COMP-5, n 5 to 9

int

PIC S9(n) COMP-5, n 10 to 18

long

COMP-1, COMP-2

float, double

packed (COMP-3) or zoned (DISPLAY) numeric

java.math.BigDecimal

a PIC X with 88 values X'00' and X'01'

boolean

a table OCCURS of one of the above

the matching array (int[], double[], String[], BigDecimal[], …​)

any other alphanumeric group

byte[]

Three rules follow from it. A parameter that corresponds to a primitive type, a String or a BigDecimal is read-only to COBOL (a change is not seen by the Java caller), whatever the passing mode; a single-dimension array of a primitive type is shared, so COBOL updates are visible to Java; and a fixed OCCURS table must have exactly as many elements as the Java array it receives (no OCCURS DEPENDING ON), or a java.lang.IllegalArgumentException is thrown. A group that does not fit an array pattern is a plain byte array, and the Java code must lay the bytes out as the COBOL group defines them; the compiler warns with message IGYPA3415I when it makes that assumption. Packed and zoned numbers become BigDecimal by conversion routines of IBM’s JZOS toolkit, and the guide warns that passing large arrays of BigDecimal or String is expensive. On the COBOL side, COMP-1 and COMP-2 in the table above are IBM hexadecimal floating point by default, converted to and from IEEE for Java.

Calling Java from COBOL

A COBOL program that writes an audit line through a Java logging class uses an ordinary CALL with a literal target. The class name is qualified with its package, the nested classes are introduced by a dollar sign, the Java. prefix is not case-sensitive and the rest is. Arguments convert by the same table.

      * z/OS; not run here. COBOL calling a static Java method
       WORKING-STORAGE SECTION.
       01  WS-MESSAGE         PIC X(40) VALUE "Fine assessed".
       PROCEDURE DIVISION.
           CALL 'Java.circulation.AuditLog.write' USING WS-MESSAGE
               ON EXCEPTION
                   DISPLAY "Java call failed"
           END-CALL
// z/OS; not run here
package circulation;

public class AuditLog {
    public static void write(String message) {
        System.out.println(message.trim());
    }
}

Only static methods can be called. Every exception during the call, on the Java side or in the generated call stub, is handled by ON EXCEPTION: the exception is cleared before the phrase runs, and the Java exception object is available through the special register IGY-JAVAIOP-CALL-EXCEPTION, from which the JNI function Throw() can propagate it. Without ON EXCEPTION an exception stops the run unit. In the other direction, an exception inside the stub’s JNI calls goes straight back to Java for the Java code to catch. A static Java method can also catch its own exceptions and return a code, which the COBOL program tests like any RETURNING result.

Sharing WORKING-STORAGE

JAVA-SHAREABLE ON and JAVA-SHAREABLE OFF bracket 01 and 77 level items of the WORKING-STORAGE SECTION that Java may read and write through the generated strg class (strg.PROGRAM.ITEM.get() and .put(...), with an index for a table element). The program has to have been called at least once, so that its storage is initialized; shared names must be unique across the COBOL part of the application and at most 26 characters long, because the mechanism uses EXTERNAL items; and a shareable item of a type that is not Java-compatible is ignored.

JAVAIOP and cjbuild

The JAVAIOP compiler option has these suboptions (it can be given only for the first program of a batch compilation):

Suboption Meaning

OUTPATH('dir')

The z/OS UNIX directory for the stub files (the default is the current directory). It must exist before the compiler runs and is at most 1023 characters; JAVAIOP(OUTPATH) without a directory is not accepted.

JAVA64 or NOJAVA64

Target 64-bit Java instead of 31-bit Java; the default is NOJAVA64. Valid only with LP(32); with LP(64) 64-bit Java is required. Java object references are then treated as 8 bytes instead of 4.

JVMINITOPTIONS('string') or NOJVMINITOPTIONS

Initialization string (at most 256 characters) used when the application starts on the COBOL side and COBOL has to start the JVM. The environment variable COBJVMINITOPTIONS does the same.

cjbuild runs from the z/OS UNIX shell or from JCL through BPXBATCH, and writes the DLL to a z/OS UNIX directory or to a data set. Its options in the 6.5 Programming Guide are:

Option Meaning

-c, --coboldir

Directory with the stub files from the compiler (default: the current directory).

-d, --dlloutdir

Where the DLL goes; for a data set, //DATASET.NAME.

-i, --progid

Process only the named program’s stubs; repeatable. A program file with the names can be given instead.

-j, --javaclassdir

Where the generated .class files go; put it in CLASSPATH.

-s, --javasrcdir

Where the generated Java source goes.

-p, --pkgname

Package of the generated progs, strg and cobol classes (default enterprise.COBOL); give each application its own.

-r, --jar

Also create a .jar from the generated classes.

-k, --cleanscript

Name of the cleanup script (default cjbuild_clean.sh).

-v, --verbose, -h, --help, --version

Show the commands run, help, and the version.

-m, --mode

PURE31, PURE64 or MIX_31_64. Marked deprecated in the 6.5 Programming Guide: it is no longer required because the mode is deduced from the stub files.

The last argument is the library base name, from which the DLL is named lib<name> on the UNIX file system and, in the 6.5 documentation, <NAME> for a data set. The guide states that before Enterprise COBOL 6.5 the LIB prefix was also added to the member name of a data set, so a build job that links against the side deck must drop the prefix when it is moved to 6.5. The program file (the list of programs) became an optional argument, so that all stub files in the directory are processed by default.

The three AMODE combinations of chapter 48 and their build flags are:

Application COBOL option cjbuild mode (deprecated)

AMODE 31 COBOL with 31-bit Java

LP(32) (default)

PURE31

AMODE 31 COBOL with AMODE 64 Java

LP(32) and JAVAIOP(JAVA64)

MIX_31_64

AMODE 64 COBOL with AMODE 64 Java

LP(64)

PURE64

For the mixed combination at run time, the guide sets LIBPATH to the Java library directories and exports IBM_JAVA_OPTIONS="-XX:+Enable3164Interoperability". The OO framework is not supported with AMODE 64 Java, which is one more reason to prefer the non-OO route for new work. Language-wise, AMODE 64 COBOL is described on Pointers and Dynamic Storage (POINTER-32 and LP(64)).

Java 25 or later asks for one more setting: the guide says to specify the JVM option --enable-native-access=ALL-UNNAMED to avoid warnings that JNI is a restricted function, through COBJVMINITOPTIONS for an application that starts in COBOL (or the JVMINITOPTIONS suboption), and in the usual Java configuration for one that starts in Java. Support for running with Semeru Java 25 arrived as a 6.5 PTF (PH70635, March 2026, per IBM’s What’s new page), and the other service-stream changes of 6.5 are summarized on Compilers and Dialects.

The Java side of this chain, compiling, packaging and the module system, is on the Java reference: Build and Tooling (Java) and Exceptions (Java); BigDecimal against COBOL decimal arithmetic is on Numbers and Math (Java).

Choosing a route

Goal Route Page section

A COBOL batch program needs a small routine written in C

CALL "name" with the C file on the cobc command line

GnuCOBOL calling C

A C or C++ application needs a COBOL business rule

cob_init, then cob_call, cob_resolve or the entry point

C calling COBOL

A Python script needs the same rule

ctypes, cob_init, FINECALC.so

Python with ctypes

A mixed C and COBOL program on z/OS

LE ILC, with or without #pragma linkage

z/OS: Language Environment

A Java program on z/OS must call an existing COBOL program

JAVA-CALLABLE, JAVAIOP, cjbuild

Making a program callable from Java

COBOL must call a Java library on z/OS

CALL of a Java. literal

Calling Java from COBOL

An object-oriented design with COBOL classes

the OO framework

Object Oriented COBOL

A service boundary instead of a library boundary

JSON or XML over a network

JSON GENERATE and PARSE; XML GENERATE and PARSE

The last two lines are the other end of the same decision: when the callers are far apart, a message format is usually better than a linked call, and the Circulation daily summary leaves the batch run as JSON for that reason (see JSON GENERATE and PARSE). Rust and other languages meet COBOL through the C interface in the same way; the other side is described at FFI and C Interop (Rust).

Common mistakes

  • Forgetting to start the run time. A C or Python caller that skips cob_init stops at the first COBOL statement with libcob: error: cob_init() has not been called.

  • Treating a COBOL string as a C string. A PIC X(n) field is space-padded and has no NUL byte; terminate a copy or pass the length.

  • Expecting a number to be an integer. DISPLAY fields are ASCII digits, packed fields are nibbles and COMP is big-endian; only BINARY-LONG, BINARY-DOUBLE, COMP-5 and floating-point items match C types directly.

  • BY VALUE on an alphanumeric item. cobc -Wall reports BY CONTENT assumed for alphanumeric item, and the C function receives an address instead of a value.

  • Relying on the default size of BY VALUE across versions. In 64-bit GnuCOBOL 3.2 the default is 64 bits; use SIZE IS 4 for a 32-bit argument or compile both sides with the same version.

  • Finding a C function by name in a shared object. The module search matches the file name of the shared object, so use COB_PRE_LOAD, link the C file into the executable, or give each function its own module.

  • cob_stop_run in a host language that buffers output. It calls exit and can lose Python’s buffered output when stdout is not a terminal; use cob_tidy and let the host exit.

  • Using the pragma on z/OS out of habit. Calling C is fastest without #pragma linkage(...,COBOL); older compilers (VS COBOL II, COBOL/370) are the ones that require it.

  • Mixing XPLINK C and COBOL in one module. An XPLINK C routine must reside in a separate module from its COBOL caller.

  • A fixed table that does not match the Java array. The OCCURS count must equal the array length or java.lang.IllegalArgumentException is thrown; OCCURS DEPENDING ON is not allowed.

  • Expecting a primitive parameter to be updated. A JAVA-CALLABLE parameter that maps to a primitive, String or BigDecimal is read-only to COBOL; return the value through RETURNING or use an array.

  • Skipping the Java 25 option. On Java 25 or later, JNI is a restricted function: add --enable-native-access=ALL-UNNAMED.

  • A cjbuild link step that still adds LIB. Before 6.5 the prefix was added to data set members as well; in 6.5 it is not.