Skip to main content

Overview

OmniView uses CGO (C-Go interoperability) to integrate with Oracle Database through the ODPI-C library. This allows Go code to call native C functions for high-performance database operations, particularly for Oracle Advanced Queuing (AQ).

Architecture

┌─────────────────────────────────────┐
│      OmniView (Go)                  │
│                                     │
│  ┌───────────────────────────────┐  │
│  │  Storage Adapter (Go)         │  │
│  └──────────┬────────────────────┘  │
│             │ CGO                    │
│  ┌──────────▼────────────────────┐  │
│  │  dequeue_ops.c (C)            │  │
│  └──────────┬────────────────────┘  │
│             │                        │
│  ┌──────────▼────────────────────┐  │
│  │  ODPI-C Library               │  │
│  └──────────┬────────────────────┘  │
└─────────────┼──────────────────────┘

┌─────────────▼──────────────────────┐
│  Oracle Instant Client             │
└─────────────┬──────────────────────┘

┌─────────────▼──────────────────────┐
│  Oracle Database                   │
└────────────────────────────────────┘

How OmniView Uses CGO

Purpose

OmniView uses CGO specifically for:
  1. Oracle AQ Dequeue Operations - Efficiently dequeue messages from Oracle Advanced Queuing
  2. CLOB/BLOB Handling - Read large object data from Oracle
  3. PL/SQL Procedure Calls - Execute stored procedures with complex type mappings
  4. Performance - Direct C library calls are faster than pure Go database/sql for bulk operations

Key Files

dequeue_ops.c

329-line C implementation with:
  • DequeueManyAndExtract() - Main dequeue function
  • ExecuteDequeueProc() - PL/SQL procedure caller
  • ReadLobContent() - LOB reader
  • FreeDequeueResults() - Memory cleanup

dequeue_ops.h

24-line header with:
  • TraceMessage struct
  • TraceId struct
  • Function declarations

CGO Compiler Configuration

CGO Flags from Makefile

The Makefile exports CGO flags that Go automatically uses during compilation:
export CGO_CFLAGS = -I$(PWD)/third_party/odpi/include
export CGO_LDFLAGS = -L$(PWD)/third_party/odpi/lib -lodpi \
    -Wl,-rpath,$(PWD)/third_party/odpi/lib \
    -Wl,-rpath,$(INSTANT_CLIENT_DIR)
Breakdown:
  • -I$(PWD)/third_party/odpi/include - Include path for dpi.h
  • -L$(PWD)/third_party/odpi/lib - Library search path
  • -lodpi - Link against libodpi.dylib
  • -Wl,-rpath,... - Runtime library search paths (dylib locations)

Manual CGO Flag Override

You can manually override CGO flags:
export CGO_CFLAGS="-I/custom/path/include"
export CGO_LDFLAGS="-L/custom/path/lib -lodpi"
make build
Manual overrides will be used instead of Makefile defaults. Ensure all required paths and flags are included.

ODPI-C Library Integration

What is ODPI-C?

ODPI-C (Oracle Database Programming Interface for C) is Oracle’s modern C API for database access. It provides:
  • Simplified API compared to OCI (Oracle Call Interface)
  • High-performance native database access
  • Support for all Oracle data types including objects, collections, LOBs
  • Advanced Queuing (AQ) support

Building the ODPI Library

The ODPI library must be built before compiling OmniView:
make odpi
The make odpi target:
  1. Compiles source files:
    gcc -O2 -Wall -I/opt/oracle/instantclient_23_7/sdk/include \
        -arch arm64 -c third_party/odpi/src/*.c
    
  2. Links into shared library: macOS:
    gcc -dynamiclib -arch arm64 \
        -install_name @rpath/libodpi.dylib \
        -Wl,-rpath,/opt/oracle/instantclient_23_7 \
        -o third_party/odpi/lib/libodpi.dylib \
        third_party/odpi/build/*.o \
        -L/opt/oracle/instantclient_23_7 -lclntsh
    
    Windows:
    gcc -shared \
        -o third_party/odpi/lib/odpi.dll \
        third_party/odpi/build/*.o \
        -LC:/oracle_inst/instantclient_23_7 -loci
    
  3. Copies to output location:
    • macOS: third_party/odpi/lib/libodpi.dylib
    • Windows: third_party/odpi/lib/odpi.dll (also copied to workspace root)

ODPI Directory Structure

third_party/odpi/
├── include/
│   ├── dpi.h                 # Main ODPI-C header (1000+ declarations)
│   ├── dpi_go_helpers.h      # Helper macros for Go integration
│   └── dpi_go_helpers.c      # Helper function implementations
├── lib/
│   ├── libodpi.dylib         # macOS ARM64 shared library
│   └── odpi.dll              # Windows DLL
└── build/                    # Temporary object files (*.o)

dequeue_ops.c Implementation

The core CGO implementation provides Oracle AQ dequeue functionality.

Data Structures

From dequeue_ops.h:
typedef struct {
    char* data;       // Message payload (JSON from CLOB)
    uint64_t length;  // Payload length in bytes
} TraceMessage;

typedef struct {
    char* data;       // Message ID (RAW bytes)
    uint32_t length;  // ID length
} TraceId;

Main Function: DequeueManyAndExtract

Signature:
int DequeueManyAndExtract(
    dpiConn* conn,           // Oracle connection
    dpiContext* context,     // ODPI context for error handling
    const char* subscriberName,  // Queue consumer name
    uint32_t batchSize,      // Number of messages to dequeue
    int32_t waitTime,        // Wait time: -1=forever, 0=no wait, >0=seconds
    TraceMessage** outMessages,  // Output: array of messages
    TraceId** outIds,        // Output: array of message IDs
    uint32_t* actualCount    // Output: actual number dequeued
);
Process flow:
1. Load Oracle object types (OMNI_TRACER_PAYLOAD_ARRAY, OMNI_TRACER_RAW_ARRAY)
2. Create ODPI variables for output collections
3. Call ExecuteDequeueProc() to run PL/SQL procedure
4. Allocate memory for results
5. Iterate through payload collection:
   - Extract CLOB attribute from each message object
   - Read CLOB content with ReadLobContent()
   - Store in TraceMessage struct
6. Iterate through raw ID collection:
   - Extract RAW bytes
   - Store in TraceId struct
7. Return results or cleanup on error
int DequeueManyAndExtract(...) {
    // Load Oracle object types
    dpiConn_getObjectType(conn, "OMNI_TRACER_PAYLOAD_ARRAY", ..., &payloadType);
    dpiConn_getObjectType(conn, "OMNI_TRACER_RAW_ARRAY", ..., &rawType);
    dpiConn_getObjectType(conn, "OMNI_TRACER_PAYLOAD_TYPE", ..., &objType);
    
    // Get attribute handle for JSON field
    dpiObjectType_getAttributes(objType, 1, &jsonAttr);
    
    // Create variables for collections
    dpiConn_newVar(conn, DPI_ORACLE_TYPE_OBJECT, ..., payloadType, &payloadVar, ...);
    dpiConn_newVar(conn, DPI_ORACLE_TYPE_OBJECT, ..., rawType, &rawVar, ...);
    
    // Execute PL/SQL dequeue procedure
    ExecuteDequeueProc(conn, context, subscriberName, batchSize, waitTime, 
                       payloadVar, rawVar, actualCount);
    
    // Allocate result arrays
    *outMessages = (TraceMessage*)calloc(*actualCount, sizeof(TraceMessage));
    *outIds = (TraceId*)calloc(*actualCount, sizeof(TraceId));
    
    // Extract payload messages
    dpiObject_getFirstIndex(payloadColl, &idx, &exists);
    while (exists) {
        dpiObject_getElementValueByIndex(payloadColl, idx, ..., &element);
        dpiObject_getAttributeValue(msgObj, jsonAttr, DPI_NATIVE_TYPE_LOB, &lobVal);
        (*outMessages)[outIdx].data = ReadLobContent(lobVal.value.asLOB, ...);
        
        // Next element
        dpiObject_getNextIndex(payloadColl, idx, &idx, &exists);
    }
    
    // Extract raw IDs
    dpiObject_getElementValueByIndex(rawColl, idx, ..., &rawElement);
    memcpy((*outIds)[outIdx].data, rawElement.value.asBytes.ptr, len);
    
    // Cleanup ODPI objects
    cleanup:
        dpiObjectType_release(payloadType);
        dpiVar_release(payloadVar);
        // ...
}

PL/SQL Procedure Execution

ExecuteDequeueProc function (lines 66-159) calls Oracle stored procedure:
const char* sql = "BEGIN OMNI_TRACER_API.Dequeue_Array_Events(:1, :2, :3, :4, :5, :6); END;";
Parameters:
  1. :1 - Subscriber name (VARCHAR2)
  2. :2 - Batch size (NUMBER)
  3. :3 - Wait time (NUMBER)
  4. :4 - Output payload array (OMNI_TRACER_PAYLOAD_ARRAY)
  5. :5 - Output raw ID array (OMNI_TRACER_RAW_ARRAY)
  6. :6 - Output count (NUMBER)
Error handling:
// ORA-25228: Timeout or end of fetch reached
if (errorInfo.code == 25228) {
    *outCount = 0;  // No messages available
    result = 0;     // Success (not an error)
    goto cleanup;
}

LOB Content Reader

ReadLobContent function (lines 25-51) reads CLOB/BLOB data:
static char* ReadLobContent(dpiLob* lob, uint64_t* outLength) {
    // Get LOB size
    dpiLob_getSize(lob, &size);
    
    // Allocate buffer
    char* buffer = (char*)malloc(size);
    
    // Read LOB data
    uint64_t bytesRead = size;
    dpiLob_readBytes(lob, 1, size, buffer, &bytesRead);
    
    *outLength = bytesRead;
    return buffer;  // Caller must free()
}
The caller is responsible for freeing the returned buffer using FreeDequeueResults().

Memory Management

FreeDequeueResults function (lines 316-329) cleans up allocated memory:
void FreeDequeueResults(TraceMessage* messages, TraceId* ids, uint32_t count) {
    if (messages) {
        for (uint32_t i = 0; i < count; i++) {
            if (messages[i].data) free(messages[i].data);
        }
        free(messages);
    }
    if (ids) {
        for (uint32_t i = 0; i < count; i++) {
            if (ids[i].data) free(ids[i].data);
        }
        free(ids);
    }
}
Always call FreeDequeueResults() after processing messages to avoid memory leaks. Each message and ID allocates heap memory that must be freed.

Using CGO in Go Code

While we don’t have the Go source files, here’s how CGO is typically used to call dequeue_ops.c:
package oracle

// #cgo CFLAGS: -I${SRCDIR}/../../../../third_party/odpi/include
// #cgo LDFLAGS: -L${SRCDIR}/../../../../third_party/odpi/lib -lodpi
// #include "dequeue_ops.h"
// #include <stdlib.h>
import "C"
import "unsafe"

func DequeueMessages(conn *C.dpiConn, ctx *C.dpiContext, subscriber string, batchSize int, waitTime int) ([]Message, error) {
    // Convert Go string to C string
    cSubscriber := C.CString(subscriber)
    defer C.free(unsafe.Pointer(cSubscriber))
    
    var cMessages *C.TraceMessage
    var cIds *C.TraceId
    var actualCount C.uint32_t
    
    // Call C function
    result := C.DequeueManyAndExtract(
        conn, ctx, cSubscriber,
        C.uint32_t(batchSize),
        C.int32_t(waitTime),
        &cMessages, &cIds, &actualCount,
    )
    
    if result != 0 {
        return nil, errors.New("dequeue failed")
    }
    
    // Convert C results to Go
    messages := make([]Message, int(actualCount))
    for i := 0; i < int(actualCount); i++ {
        msg := C.GetMessage(cMessages, C.int(i))  // Helper to get array element
        messages[i] = Message{
            Data: C.GoBytes(unsafe.Pointer(msg.data), C.int(msg.length)),
            ID:   // ... extract ID
        }
    }
    
    // Free C memory
    C.FreeDequeueResults(cMessages, cIds, actualCount)
    
    return messages, nil
}

Debugging CGO Issues

View CGO Compilation

See exactly how CGO compiles your C code:
make check-cgo
This shows:
  • CGO compiler flags
  • CGO linker flags
  • GCC/Clang command lines
  • Which .c files are being compiled
[DEBUG] Checking CGO compilation...
CGO_CFLAGS=-I/Users/dev/omniview/third_party/odpi/include
CGO_LDFLAGS=-L/Users/dev/omniview/third_party/odpi/lib -lodpi -Wl,-rpath,/Users/dev/omniview/third_party/odpi/lib -Wl,-rpath,/opt/oracle/instantclient_23_7

Building oracle storage package with debug output:
cd /Users/dev/omniview/internal/adapter/storage/oracle
gcc -I. -fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments \
    -fmessage-length=0 -fdebug-prefix-map=$WORK=/tmp/go-build \
    -gno-record-gcc-switches -fno-common \
    -I/Users/dev/omniview/third_party/odpi/include \
    -o $WORK/b001/dequeue_ops.o -c dequeue_ops.c

Enable CGO Debug Mode

Get verbose CGO output during build:
CGO_ENABLED=1 go build -x ./internal/adapter/storage/oracle
The -x flag prints all commands as they execute, including:
  • CGO preprocessing
  • C compilation
  • Object file linking

Common CGO Errors

Problem: ODPI library not found or not linked properly.Solution:
# Rebuild ODPI library
make clean
make odpi

# Verify library exists
ls -la third_party/odpi/lib/

# Check CGO flags
echo $CGO_LDFLAGS
Problem: CGO_CFLAGS not set or incorrect include path.Solution:
# Verify include directory exists
ls third_party/odpi/include/dpi.h

# Set CGO_CFLAGS manually
export CGO_CFLAGS="-I$(pwd)/third_party/odpi/include"
make build
Problem: Linker can’t find libodpi.dylib.Solution:
# Check library exists
ls third_party/odpi/lib/libodpi.dylib

# Rebuild if missing
make odpi

# Set library path
export DYLD_LIBRARY_PATH=$(pwd)/third_party/odpi/lib:$DYLD_LIBRARY_PATH
Problem: odpi.dll or Oracle DLLs not in PATH.Solution:
# Copy DLL to workspace root
copy third_party\odpi\lib\odpi.dll .

# Add Oracle Instant Client to PATH
set PATH=%PATH%;C:\oracle_inst\instantclient_23_7

Troubleshooting CGO Issues

Runtime Library Loading

Ensure shared libraries are found at runtime:
# Check what libraries the binary needs
otool -L ./omniview

# Output should show:
# @rpath/libodpi.dylib
# /opt/oracle/instantclient_23_7/libclntsh.dylib

# Set runtime search path
export DYLD_LIBRARY_PATH=$(pwd)/third_party/odpi/lib:/opt/oracle/instantclient_23_7
./omniview

CGO Memory Issues

CGO has important memory rules:
  • C code allocates memory with malloc() - must free() in C
  • Go code passes pointers to C - must not move during C call
  • Use C.CString() for Go→C strings (remember to free!)
  • Use C.GoString() or C.GoBytes() for C→Go data
Common memory mistakes:
// ❌ WRONG: String gets freed before C function returns
name := "subscriber"
result := C.DoSomething(C.CString(name))

// ✅ CORRECT: Keep pointer alive and free after
cName := C.CString(name)
defer C.free(unsafe.Pointer(cName))
result := C.DoSomething(cName)

// ❌ WRONG: Go pointer might move during C call
var data []byte
C.ProcessData(unsafe.Pointer(&data[0]))  // Dangerous!

// ✅ CORRECT: Pin memory or copy to C-allocated buffer
cData := C.malloc(C.size_t(len(data)))
defer C.free(cData)
C.memcpy(cData, unsafe.Pointer(&data[0]), C.size_t(len(data)))

Oracle Connection Issues

If dequeue operations fail:
// Enable ODPI-C error reporting in dequeue_ops.c
#define CHECK_DPI(status, ctx, msg) \
    if (status != DPI_SUCCESS) { \
        dpiErrorInfo errorInfo; \
        dpiContext_getError((ctx), &errorInfo); \
        fprintf(stderr, "[C ERROR] %s: %.*s\n", \
                (msg), (int)errorInfo.messageLength, errorInfo.message); \
        return -1; \
    }
All ODPI errors are printed to stderr with full Oracle error messages.

Environment Variables

Required for Building

# Oracle Instant Client location
export INSTANT_CLIENT_DIR=/opt/oracle/instantclient_23_7

# CGO must be enabled (default is 1)
export CGO_ENABLED=1

Optional for Development

# Force CGO to use specific compiler
export CC=gcc-11

# Add extra compiler flags
export CGO_CFLAGS="$CGO_CFLAGS -DDEBUG -g"

# Add extra linker flags
export CGO_LDFLAGS="$CGO_LDFLAGS -v"

Performance Considerations

Why Use CGO?

CGO adds complexity but provides benefits for OmniView:

Performance

Direct C calls to Oracle are 2-5x faster than database/sql for bulk operations

Advanced Features

ODPI-C supports Oracle-specific features like AQ, object types, and collections

Memory Efficiency

Streaming LOB data without intermediate buffers

Batch Operations

Dequeue multiple messages in single call

CGO Overhead

CGO calls have ~10-50ns overhead per call:
  • Good: Batch operations (dequeue 100 messages)
  • Bad: Per-row processing (call C for each row)
OmniView uses CGO efficiently by batching dequeue operations.

Best Practices

1

Always Free C Memory

Every malloc() in C code must have corresponding free(). Use defer in Go.
2

Check Error Returns

All ODPI-C functions return status codes. Always check them.
3

Use defer for Cleanup

Release ODPI objects in reverse order of acquisition using defer.
4

Handle NULL Values

Check isNull flag before accessing ODPI data values.
5

Test with CGO_ENABLED=1

Some bugs only appear when CGO is actually used.

Next Steps

Building from Source

Complete guide to building OmniView

Makefile Reference

All available make targets explained

Build docs developers (and LLMs) love