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.
Source: GnuCOBOL 3.2 manual: C interface
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) COMPis stored big-endian (00 00 00 01), as on the mainframe, regardless of the machine. A Cinton this machine is little-endian, so C cannot read it directly. GnuCOBOL’sbinary-byteordersetting controls this, andCOMPis the form that matches z/OS data files. -
PIC S9(9) COMP-5andBINARY-LONGare the machine’s native order (01 00 00 00here), the same as a Cint.BINARY-LONGis the native 32-bit integer by definition;COMP-5is the native form that keeps the digit count of thePICTURE. -
A
DISPLAYnumber is one character per digit, with the decimal point implied:5.00inPIC 9(5)V99is the seven bytes30 30 30 30 35 30 30, the ASCII characters0000500. 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.25is00 00 52 5D, the digits0000525and the signD(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 |
|---|---|---|
|
|
Native byte order, so they match without conversion. |
|
|
64 bits on 64-bit Linux, 32 bits on 64-bit Windows: it follows the C compiler. |
|
|
Native order; the digit count selects the size. |
|
|
IEEE binary floating point in GnuCOBOL. On z/OS the same names are IBM hexadecimal floating point by default (see below). |
|
|
Not NUL-terminated and padded with spaces: add the terminator or pass the length. |
|
|
The decimal point is not stored. |
|
none |
Big-endian binary and packed decimal: decode by hand. |
|
|
A C function can return an address that COBOL takes with |
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 REFERENCEhands C a pointer toWS-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-DAYSpasses the number itself, not an address.RETURNING WS-CENTSstores the C function’sintresult into aBINARY-LONG. -
The C function names are case-sensitive and must match the literal in the
CALL; a COBOL program name, by contrast, is thePROGRAM-IDas 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.
Source: GnuCOBOL 3.2 manual: Dynamic linking with COBOL programs; Static linking with COBOL programs
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
LINKAGEitemLK-DAYSisBINARY-LONG, a native 32-bit integer, soc_int32is the matchingctypestype; aPIC 9(3)V99result is five characters, so a five-byte buffer is passed. ACOMPitem would arrive big-endian and needint.from_bytes(..., "big"). -
Without
cob_init, the script ends at the first call withlibcob: error: cob_init() has not been calledand exit status 1, the same message the C program printed. -
Ending the script with
libcob.cob_stop_run(0)instead ofcob_tidyloses the output when it is piped or redirected.cob_stop_runcalls the C library’sexit, which skips Python’s own output buffer:python3 fines_stoprun.py | headandpython3 fines_stoprun.py > out.txtboth produced no output at all, although the exit status was 0, while the same script withcob_tidy(or withpython3 -u) printed all three lines. A Python caller should usecob_tidy, or flushsys.stdoutfirst.
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
NODLLcan call C by a static or a dynamicCALL, 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 withDLLcan 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 COBOLCANCELcannot be used on a program that was loaded byfetch(); C usesrelease(). -
XPLINK. C compiled with
XPLINKis 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
voidfunction 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-CALLABLEdirective makes a COBOL program a native method of a Java class. -
The
JAVA-SHAREABLEdirective exposesWORKING-STORAGEitems to Java for reading and writing. -
A
CALLwhose target is a literal of the form'Java.class-name.static-method-name'calls a static Java method. -
The
JAVAIOPcompiler option controls where the compiler writes the stub files and which Java the program targets. -
The
cjbuildutility 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 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
packed ( |
|
a |
|
a table |
the matching array ( |
any other alphanumeric group |
|
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 |
|---|---|
|
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; |
|
Target 64-bit Java instead of 31-bit Java; the default is |
|
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 |
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 |
|---|---|
|
Directory with the stub files from the compiler (default: the current directory). |
|
Where the DLL goes; for a data set, |
|
Process only the named program’s stubs; repeatable. A program file with the names can be given instead. |
|
Where the generated |
|
Where the generated Java source goes. |
|
Package of the generated |
|
Also create a |
|
Name of the cleanup script (default |
|
Show the commands run, help, and the version. |
|
|
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 |
|
|
AMODE 31 COBOL with AMODE 64 Java |
|
|
AMODE 64 COBOL with AMODE 64 Java |
|
|
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.
Source: Enterprise COBOL for z/OS 6.5 Programming Guide (PDF): JAVAIOP; cjbuild command reference; AMODE considerations; IBM, What’s new in Enterprise COBOL for z/OS 6.5
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 |
|
GnuCOBOL calling C |
A C or C++ application needs a COBOL business rule |
|
C calling COBOL |
A Python script needs the same rule |
|
Python with ctypes |
A mixed C and COBOL program on z/OS |
LE ILC, with or without |
z/OS: Language Environment |
A Java program on z/OS must call an existing COBOL program |
|
Making a program callable from Java |
COBOL must call a Java library on z/OS |
|
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_initstops at the first COBOL statement withlibcob: 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.
DISPLAYfields are ASCII digits, packed fields are nibbles andCOMPis big-endian; onlyBINARY-LONG,BINARY-DOUBLE,COMP-5and floating-point items match C types directly. -
BY VALUEon an alphanumeric item.cobc -WallreportsBY CONTENT assumed for alphanumeric item, and the C function receives an address instead of a value. -
Relying on the default size of
BY VALUEacross versions. In 64-bit GnuCOBOL 3.2 the default is 64 bits; useSIZE IS 4for 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_runin a host language that buffers output. It callsexitand can lose Python’s buffered output when stdout is not a terminal; usecob_tidyand 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
OCCURScount must equal the array length orjava.lang.IllegalArgumentExceptionis thrown;OCCURS DEPENDING ONis not allowed. -
Expecting a primitive parameter to be updated. A
JAVA-CALLABLEparameter that maps to a primitive,StringorBigDecimalis read-only to COBOL; return the value throughRETURNINGor 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
cjbuildlink step that still addsLIB. Before 6.5 the prefix was added to data set members as well; in 6.5 it is not.