Part 7 · Advanced & Integration · Intermediate

Exports & Context Calls

export "DPI-C" to let C call SV, context imports, svSetScope/svGetScope, and the callback-into-TB pattern.

Exports: the reverse direction

An export "DPI-C" declaration makes a SystemVerilog function or task callable from C. The export is written inside the scope (module, interface, package, or program) that defines the function — and that scope matters, because the C side must call the export in the context of a specific instance of that scope.

systemverilog
module tb_top;
  int unsigned err_count;

  // SV function that C will call
  function void report_c_error(input int code, input string msg);
    err_count++;
    $display("[C-MODEL] error %0d: %s", code, msg);
  endfunction

  // Make it visible to C — declared in the same scope
  export "DPI-C" function report_c_error;

  // The context import that will eventually trigger the callback
  import "DPI-C" context function void c_model_step(input int unsigned d);
endmodule

Why exports require context imports

C can only call an exported SV function while the simulator knows which instance scope the call belongs to. That scope is established when SV calls into C through a context import: the simulator records the call site, and any export the C code invokes during that call executes in the caller's scope. A non-context import gives C no scope, so calling an export from it is illegal.


svSetScope and svGetScope

Sometimes C needs to call an export later — from a different import call, or after stashing state — not just during the original call. The scope API makes scope a first-class value: svGetScope() captures the current scope as an svScope handle, and svSetScope() restores it before invoking the export.

c
// c_model.c
#include "svdpi.h"

/* prototype of the exported SV function */
extern void report_c_error(int code, const char* msg);

static svScope tb_scope;   /* captured scope for later callbacks */

void c_model_init(void)
{
    /* called via a context import; remember where we live */
    tb_scope = svGetScope();
}

void c_model_step(unsigned int data)
{
    if (data == 0xDEADu) {
        /* restore scope before calling back into SV */
        svSetScope(tb_scope);
        report_c_error(42, "illegal data word");
    }
}
diagram
SCOPE FLOW [SV] ↔ [C]

  tb_top (instance scope)
     │
     │  c_model_init()        ← context import call
     ▼
  [C] svGetScope() ──► tb_scope saved
     ...
     │  c_model_step(0xDEAD)  ← later context import call
     ▼
  [C] svSetScope(tb_scope)
     │
     │  report_c_error(42,...)  ← export executes in tb_top scope
     ▼
  [SV] err_count++ inside tb_top

Pattern: C model signals an event to the TB

The classic use: a C model detects something asynchronously to the SV call flow (end of a frame, an internal error) and needs to wake the testbench. C cannot touch SV events directly, but it can call an exported function that does.

systemverilog
module c_bridge;
  event frame_done;
  int   frame_len;

  function void notify_frame_done(input int len);
    frame_len = len;
    -> frame_done;                 // wake any waiting TB process
  endfunction
  export "DPI-C" function notify_frame_done;

  import "DPI-C" context function void c_push_byte(input byte b);

  // TB side consumes the event like any other
  task automatic wait_frame(output int len);
    @(frame_done);
    len = frame_len;
  endtask
endmodule
c
// frame_model.c
#include "svdpi.h"
extern void notify_frame_done(int len);

static int count = 0;

void c_push_byte(char b)
{
    count++;
    if ((unsigned char)b == 0x7E) {   /* frame delimiter */
        notify_frame_done(count);      /* context call back into SV */
        count = 0;
    }
}

Walkthrough

  1. c_push_byte is a context import — every call carries the c_bridge instance scope.

  2. On the delimiter byte, C calls the export, which fires the SV event in that scope.

  3. Any TB process blocked in wait_frame() resumes — C indirectly controlled SV scheduling.

  4. No svSetScope needed here because the callback happens during the import call itself.

Key takeaways

  • export "DPI-C" makes an SV function/task callable from C; declare it in the defining scope.

  • C may only call exports with a valid scope — established by a context import or svSetScope.

  • svGetScope/svSetScope let C stash a scope and call back later — the basis of persistent C-model callbacks.

  • C signals the TB by calling an export that fires an SV event — never by touching SV objects directly.

Common pitfalls

  • Calling an export from a non-context import — undefined behavior; usually a crash or scope error.

  • Forgetting svSetScope before a deferred callback — the export runs in the wrong (or stale) instance.

  • Exporting a blocking task and calling it from a C function import — functions cannot consume time; use task imports.

  • Multiple TB instances sharing one C model with a single static svScope — callbacks all land in one instance.