Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • SystemVerilog Language Foundations34
  • Object-Oriented SystemVerilog for Verification33
  • Constraint Randomization — Interview-Critical Deep Dive56
  • SystemVerilog Assertions — Interview-Critical Deep Dive54
  • Functional Coverage in SystemVerilog28
  • Testbench Architecture in SystemVerilog32
  • Advanced Language & Integration30
    • DPI-C: Calling C from SystemVerilog
      • DPI Imports
      • Exports & Context Calls
      • Passing Arrays & Structs
      • C Reference Models
      • DPI Pitfalls & Debug
    • bind: Attaching Verification to RTL
    • Macros, Defines & Compilation
    • Plusargs, Files & Runtime Configuration
    • Simulation Performance & TB Memory
    • RTL/TB Boundaries, Races & Timing
  • Senior & Interview Preparation29
Loading lesson

‹DPI-C: Calling C from SystemVerilog
  1. Topics
  2. SystemVerilog
  3. Advanced Language & Integration
  4. DPI-C: Calling C from SystemVerilog
  5. Exports & Context Calls

Part 7 · Advanced & Integration · Intermediate

Exports & Context Calls

Export SV functions for C callbacks, with context scope rules and the event wake-up pattern.

On this page

    Exports in the defining scope

    An export declaration makes a SystemVerilog function or task callable from C. The export is written in the scope that defines the function.

    That scope matters because C must call the export in the context of a specific instance of that scope.

    The next listing declares an export with the context import that triggers it. It needs its C half linked through DPI-C, so it is not run in this course's checks.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // not-run: needs its C half compiled and linked; the course runner builds SV only (Verilator 5.048 links DPI-C, Icarus 13 has no DPI)
    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;
    
      // Context imports that C uses; init captures the scope
      import "DPI-C" context function void c_model_init();
      import "DPI-C" context function void c_model_step(input int unsigned d);
    endmodule

    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.

    A non-context import gives C no scope, so calling an export from it is illegal.

    Common pitfall

    Calling an export from a non-context import is undefined behavior. It usually ends as a crash or a scope error far from the call.

    Check yourself A C model must call an exported SV function. What must the SV side declare so the call has a scope?

    A context import. The simulator records the call site of the context import, and exports that C invokes during the call run in the caller scope.

    Scope handles

    Deferred callbacks need a scope that outlives one import call. The scope API makes scope a value: svGetScope captures it and svSetScope restores it.

    The next listing stores the scope at init time and restores it before a later callback. It needs its C half linked through DPI-C, so it is not run in this course's checks.

    c
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // not-run: C half of a DPI pair; the course runner builds SV only (Verilator 5.048 compiles it as C++, so wrap it in extern "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
         v
      [C] svGetScope() --> tb_scope saved
         ...
         |  c_model_step(0xDEAD)  <- later context import call
         v
      [C] svSetScope(tb_scope)
         |
         |  report_c_error(42,...)  <- export runs in tb_top scope
         v
      [SV] err_count++ inside tb_top
    Check yourself Two testbench instances share one C model with a single static scope handle. Where do deferred callbacks land?

    In whichever instance was captured last. Each instance needs its own stored scope, or callbacks land in the wrong place.

    Event callback pattern

    A C model often detects something the testbench must react to, such as a frame end. C cannot touch SV events directly, so it calls an export that fires one.

    The next listing shows the SV half of that pattern. It needs its C half linked through DPI-C, so it is not run in this course's checks.

    c_bridge.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // not-run: needs its C half compiled and linked; the course runner builds SV only (Verilator 5.048 links DPI-C, Icarus 13 has no DPI)
    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

    The C half counts bytes and fires the export on the delimiter. It needs its C half linked through DPI-C, so it is not run in this course's checks.

    c
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // not-run: C half of a DPI pair; the course runner builds SV only (Verilator 5.048 compiles it as C++, so wrap it in extern "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;
        }
    }

    The cast to unsigned char is deliberate. SV byte is signed and C char signedness varies, so the comparison converts explicitly.

    • c_push_byte is a context import, so every call carries the c_bridge instance scope.

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

    • Any testbench process blocked in wait_frame resumes, so C indirectly controls SV scheduling.

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

    Interview

    Expect to trace the scope of a callback: which import carried it, whether svSetScope was needed, and what breaks with several instances sharing one stored handle.

    Testbench

    The next listing proves the same event wake-up in plain SystemVerilog. It runs in this course's checks.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    timeunit 1ns; timeprecision 1ps;
    
    module tb_event_callback;
      event frame_done;
      int frame_len;
      int got_len;
      int bad;
      int count;
    
      task automatic notify_frame_done(input int len);
        frame_len = len;
        -> frame_done;
      endtask
    
      task automatic push_byte(input logic [7:0] b);
        count++;
        if (b == 8'h7E) notify_frame_done(count);
      endtask
    
      initial begin
        bad = 0; count = 0; frame_len = 0; got_len = 0;
        fork
          begin @(frame_done); got_len = frame_len; end
          begin
            #1;
            push_byte(8'h11); push_byte(8'h22); push_byte(8'h7E);
          end
        join
        if (got_len !== 3) bad++;
        if (bad == 0) $display("PASS: event callback carried length 3 to the waiter");
        else $fatal(1, "FAIL: event callback carried a wrong length");
        $finish;
      end
    endmodule
    Check yourself The driver branch waits one unit before pushing. What breaks if that delay is removed?

    The wake-up can fire before the waiter blocks on the event. The waiter then blocks forever, since the trigger is already gone.

    Related topics

    • Passing Arrays & Structs

    Previous

    239 / 296

    Next: Passing Arrays & Structs

    Up next

    Up nextPassing Arrays & Structs

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • C Reference Models
    • DPI Pitfalls & Debug