Many functionalities on z/OS are exposed through High Level Assembly (HLASM) macros, and you may want to be able to take advantage of these when using IBM Open Enterprise SDK for Python. In this blog, we’re going to cover how you can create a call some HLASM from Python, both when it’s being inlined into C and as straight assembly.
Using Inline ASM
The way that we’re going to run some HLASM is by creating a package in C using setuptools, which can then call our HLASM. Setuptools is the standard way of creating an installable Python package that can be distributed. For this example, we’re going to be running the WTL macro, which writes a message to the operator's log SYSLOG. To do this, we’re going to need to set up a new directory as follows:
.
└── zlog/
├── setup.py
└── src/
└── zlog.c
zlog is the projects name. setup.py will contain all of the information required to build the project. zlog.c will be the C code that contains the inline HLASM, and information required for Python to be able to call it. Let’s look at both:
Setup.py:
from setuptools import setup, Extension
def main():
setup(
name="zlog",
version="1.0.0",
ext_modules=[
Extension(
"zlog",
sources = ["src/zlog.c"]
)
],
)
if __name__ == "__main__":
main()
The setup.py for building this project is small, and just uses setuptools to configure the build of our package. It specifies the name of our package, version, and the required C files which are going to be compiled, which in this case is a single file called zlog.c.
zlog.c:
#include <unistd.h>
#include <stdio.h>
#include <Python.h>
#define WTLMAXLENGTH 126
struct WTLParm
{
short int len;
short int flags;
unsigned char message[WTLMAXLENGTH];
};
static PyObject * write_to_log(PyObject *self, PyObject *args){
// Get the message to log to the console
const char * message;
if (!PyArg_ParseTuple(args, "s", &message)) {
PyErr_SetString(PyExc_ValueError, "Invalid message");
return NULL;
}
// WTL Takes in a limited buffer, make sure we only log that amount
int length = strlen(message);
if (length >= WTLMAXLENGTH) {
PyErr_SetString(PyExc_ValueError, "Message length too long");
return NULL;
}
// Set up parameters for calling WTL
// Addresses passed in need to be below the bar
// https://www.ibm.com/docs/en/zos/3.1.0?topic=log-wtl-execute-form
struct WTLParm * wtlparm = __malloc31(sizeof(struct WTLParm));
strncpy(wtlparm->message, message, WTLMAXLENGTH);
wtlparm->len = length + 4;
wtlparm->flags = 0;
// Python operates on UTF-8 encoding. Convert it to EBCDIC
__a2e_s(wtlparm->message);
// Call WTL to write to the operator log
__asm(" WTL MF=(E,(%0))"
:
: "r"(wtlparm)
: "r0","r1","r14","r15");
free(wtlparm);
return Py_BuildValue("i", 0);
}
// Methods for our zlog package
static PyMethodDef methods[] = {
{"write_to_log", write_to_log, METH_VARARGS, "Write a UTF-8 string to the operator log"},
{NULL, NULL, 0, NULL}
};
// Definition of the zlog package
static struct PyModuleDef zlog_module = {
PyModuleDef_HEAD_INIT,
"zlog",
NULL,
-1,
methods
};
// Entry point when a user imports zlog
PyMODINIT_FUNC PyInit_zlog(void)
{
return PyModule_Create(&zlog_module);
}
Most of the code in zlog.c is boilerplate code to allow Python to import and call functions that it defines. The one function that is interesting here is the write_to_log function, which is where we have our inline HLASM defined. This function calls the WTL macro using the __asm function.
The execute form of WTL takes in parameter list - a message to print to the console, the length of that message, and flags to describe it. These are all held within the struct WTLParm. To use it from Python, we can pass in whatever message we want (in UTF-8), convert it to EBCDIC, then use the WTL macro to print it to SYSLOG.
Building this package is simple and just requires setting the appropriate compiler options:
export CFLAGS="-qasm -qasmlib=sys1.maclib -qasmlib=sys1.modgen"
export CC=/bin/xlc
export CXX=/bin/xlc++
export LDSHARED=/bin/xlc
pip3 install ./zlog
One thing to note is that when compiling this HLASM, we need to define where the WTL macro is located, and allow HLASM. In this case it’s in the sys1.modgen & sys1.maclib dataset, so we need to specify the options for the compiler to be able to find it.
This creates a module with one function that can be used as follows:
import zlog
zlog.write_to_log('A message sent to the z/OS console!')
Which when run, would output the following message to the console:

Using Separate HLASM
Instead of embedding the HLASM directly within the source of our C file, we can also separate it into its own file and link it together. This looks almost identical to the previous example with only a change to setup.py to specify the new file, and our C file which will then call the HLASM. The file tree looks as follows:
.
└── zlog/
├── setup.py
└── src/
├── hlasm_extension.c
└── asm.s
The modified setup.py with the only change being to add "src/asm.s":
from setuptools import setup, Extension
def main():
setup(
name="zlog",
version="1.0.0",
ext_modules=[
Extension(
"zlog",
sources = ["src/zlog.c", "src/asm.s"]
)
],
)
if __name__ == "__main__":
main()
This setup.py is identical to the previous one, except that it’s added “src/asm.s” to the list of sources that are needed to be compiled. Setuptools already knows how to compile HLASM files, so it will append the appropriate compiler options to be able to build this. In this case, setuptools will just compile and link the two files into a single shared library which can be imported by Python.
The HLASM here is a little more complicated than the inline HLASM seen in the first example, but still does the same task. It simply takes in a pointer to WTLParm, runs the WTL macro on it, and that prints the message to the z/OS console.
asm.s:
WTLFUNC CELQPRLG DSASIZE=DSASZ
USING CEEDSAHP,4
* Run WTL Macro. wtlparm gets passed in register 1
* since this is compiled as XPLINK
WTL MF=(E,(1))
* Return code of WTL macro is in register 15, move it to
* the XPLINK return register 3
LR 3,15
CELQEPLG
CEEDSAHP CEEDSA SECTYPE=XPLINK
DSASZ EQU (*-CEEDSAHP_FIXED)
END WTLFUNC
For the code that’s found within zlog, it’s again nearly identical. The only difference being the declaration of WTLFUNC, and the calling of it.
zlog.c:
...
extern int WTLFUNC(struct WTLParm *);
static PyObject * write_to_log(PyObject *self, PyObject *args){
// Get passed in parameter
const char * message;
if (!PyArg_ParseTuple(args, "s", &message)) {
PyErr_SetString(PyExc_ValueError, "Invalid message");
return NULL;
}
int length = strlen(message);
if (length >= WTLMAXLENGTH) {
PyErr_SetString(PyExc_ValueError, "Message length too long");
return NULL;
}
// Set up parameters for calling WTL
// Addresses passed in to WTL need to be below the bar
// https://www.ibm.com/docs/en/zos/3.1.0?topic=log-wtl-execute-form
struct WTLParm * wtlparm = __malloc31(sizeof(struct WTLParm));
strncpy(wtlparm->message, message, WTLMAXLENGTH);
wtlparm->len = length + 4;
wtlparm->flags = 0;
// Python operates on UTF-8 encoding. Convert it to EBCDIC
__a2e_s(wtlparm->message);
// Call WTL to write to the operator log
int rc = WTLFUNC(wtlparm);
free(wtlparm);
return Py_BuildValue("i", rc);
}
...
That’s it. You can now run HLASM code using Python by following these examples.