import os import subprocess import ctypes import yaml from fastapi import FastAPI, HTTPException app = FastAPI(title="COBOL REST Framework") # 1. Load the Configuration with open("api.yaml", "r") as f: config = yaml.safe_load(f) # 2. On-the-fly Compiler Pipeline # This makes it completely system independent! COMPILED_LIBS = {} for endpoint in config["endpoints"]: cbl_source = endpoint["cobol_source"] # Convert 'src/fetch_account.cbl' -> '/tmp/fetch_account.so' so_name = os.path.basename(cbl_source).replace(".cbl", ".so") so_path = os.path.join("/tmp", so_name) print(f"🛠️ Compiling legacy asset {cbl_source} to {so_path}...") # Use standard Linux GnuCOBOL compilation flags result = subprocess.run(["cobc", "-b", "-o", so_path, cbl_source], capture_output=True, text=True) if result.returncode != 0: print(f"❌ Compilation Failed:\n{result.stderr}") exit(1) COMPILED_LIBS[cbl_source] = so_path # Initialize GnuCOBOL environment globally try: libcob = ctypes.CDLL("libcob.so", mode=ctypes.RTLD_GLOBAL) libcob.cob_init() except OSError: pass def call_cobol(so_path, procedure, account_id): """Dynamically loads compiled COBOL libraries and maps network payloads to buffers""" # 1. Load the library module cobol_lib = ctypes.CDLL(so_path) # 2. Use GnuCOBOL's official runtime resolver to find our PROCEDURE/PROGRAM-ID # This acts as the bulletproof bridge between Linux and COBOL symbols try: libcob = ctypes.CDLL("libcob.so", mode=ctypes.RTLD_GLOBAL) libcob.cob_resolve.restype = ctypes.c_void_p func_ptr = libcob.cob_resolve(procedure.encode('utf-8')) except Exception: func_ptr = None # Fallback to direct symbol reading if cob_resolve isn't bound yet if not func_ptr: for name in [procedure, procedure.lower(), procedure.upper()]: if hasattr(cobol_lib, name): func_ptr = ctypes.cast(getattr(cobol_lib, name), ctypes.c_void_p).value break if not func_ptr: raise HTTPException( status_code=500, detail=f"COBOL Procedure symbol '{procedure}' could not be resolved." ) # Cast the resolved pointer into a callable Python FFI function cobol_func = ctypes.CFUNCTYPE(None, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p)(func_ptr) # 3. Map Input Argument: PIC X(10) padded_id = account_id.ljust(10)[:10].encode('ascii') id_buf = ctypes.create_string_buffer(padded_id, 10) # 4. Allocate Output Buffers matching the YAML spec sizes balance_buf = ctypes.create_string_buffer(9) # 9 bytes status_buf = ctypes.create_string_buffer(2) # 2 bytes # 5. Call out to the compiled COBOL logic cobol_func(id_buf, balance_buf, status_buf) # 6. Decode response strings status = status_buf.value.decode('ascii').strip() if status == "NF": raise HTTPException(status_code=404, detail="Account not found in COBOL storage") raw_balance = balance_buf.value.decode('ascii').strip() # Turn fixed-length string '015004500' -> 1500.45 balance = float(raw_balance[:7] + "." + raw_balance[7:9]) return {"balance": balance, "status": status} # 3. Dynamic API Route Builder for endpoint in config["endpoints"]: if endpoint["method"] == "GET" and endpoint["path"] == "/accounts/{id}": @app.get(endpoint["path"]) def get_account(id: str): so_file = COMPILED_LIBS[endpoint["cobol_source"]] proc_name = endpoint["procedure"] return call_cobol(so_file, proc_name, id) if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)