← Design Verification Interviews

Interview Question · Code Writing

Calling a C Function from the Testbench

L1 JuniorL2 Mid-Levelsystemverilogdpi
  • Likely at Analog Devices

The Question

The interviewer asks

A C reference model exposes int is_large(unsigned char len). Write the SystemVerilog side that calls it with an 8-bit burst length, and tell me which type you chose and why.

Reveal the Model Answer

Declare import "DPI-C" function int is_large(input byte unsigned len); and call it like any function. SystemVerilog byte is signed and maps to C char, so an unsigned C parameter needs byte unsigned, which maps to unsigned char. Include the simulator-generated header in the C file, so a type mismatch fails the build instead of passing wrong values.

One Call, Three FilesArrows between files show which file produces or checks which. The bottom arrow is the call at run time: the request goes right, the result comes back.
One call, three filesArrows between files show which file produces or checks which. The bottom arrow is the call at run time: the request goes right, the result comes back. SystemVerilog main.sv import "DPI-C"function intadd_small(input int a,b); generated Vtb__Dpi.h extern "C" intadd_small(int a, intb); C bridge.cpp int add_small(int a,int b) { return a + b;} binary Vtb one program: thesimulation and thebridge run time add_small(7, 5) the testbench calls, Creturns 12 Verilator writesincluded and comparedcompiled incompiled incallruns the body
  1. Declare

    The import line names the function, its result type and its two int inputs. Nothing in SystemVerilog says what it does.

  2. Generate

    Verilator reads every import and writes Vtb__Dpi.h: one C prototype per import, with C linkage.

  3. Define

    The bridge includes that header and defines the function. The compiler compares the definition with the prototype and stops on any difference.

  4. Link

    The generated simulation code and the bridge are compiled into one program. The linker joins the call to the C definition.

  5. Call

    At run time the testbench calls add_small(7, 5). C adds, returns 12, and the testbench checks the value before printing it.

    Follow the calls, values and waits
    1. call add_small(7, 5): main.sv → add_small(7, 5)
    2. call add_small(7, 5): add_small(7, 5) → bridge.cpp
    3. return 12: bridge.cpp → add_small(7, 5)
    4. return 12: add_small(7, 5) → main.sv
Read the diagram as text
  • main.sv: import "DPI-C" function int add_small(input int a, b);
  • Vtb__Dpi.h: extern "C" int add_small(int a, int b);
  • bridge.cpp: int add_small(int a, int b) { return a + b; }
  • Vtb: one program: the simulation and the bridge
  • add_small(7, 5): the testbench calls, C returns 12
  • main.sv → Vtb__Dpi.h: Verilator writes
  • Vtb__Dpi.h → bridge.cpp: included and compared
  • main.sv → Vtb: compiled in
  • bridge.cpp → Vtb: compiled in
  • main.sv → add_small(7, 5): call
  • add_small(7, 5) → bridge.cpp: runs the body

Read the three files as one unit: the import declares, the generated header carries the prototype, the bridge defines.

The import names the routine and its types; the header makes the C compiler check them.
// SystemVerilog side
import "DPI-C" function int is_large(input byte unsigned len);
...
if (is_large(burst_len)) large_bursts++;

// C side (bridge.cpp)
#include "Vtb__Dpi.h"      // prototypes generated from the import
extern "C" int is_large(unsigned char len) { return len > 127; }

Self-Check

Tick each key point you covered in your answer.

What Interviewers Listen For (Junior)

A good junior answer writes the import with an explicit direction and byte unsigned. It includes the generated header in the C file.

Cross-Questions

Where the interviewer goes next. Open a node to practise that follow-up.

WhyWhat IfBreak ItCompare

  1. Calling a C Function from the Testbench
  2. WhyWhy Include the Generated DPI HeaderL1 Junior

    Why bother including Vtb__Dpi.h in the C file when the code compiles without it?

    Reveal the Model Answer

    The simulator writes a C prototype for every import into that header. With it included, the C compiler compares your definition against the import and refuses a mismatch such as char against unsigned char. Without it, the linker matches names only, so a wrong type or direction runs and produces wrong numbers.

    Self-Check

    Tick each key point you covered in your answer.

    Half-Truth

    "The header is only needed for exported functions."

    It carries a prototype for every import too, and that is what catches type mismatches at build time.

    Source Lesson D1.1 review claims (Verilator writes Vtb__Dpi.h with a prototype per import)

  3. What IfA C++ Definition Without Extern CL1 JuniorL2 Mid-Level

    What if the C side is a C++ file and you forget extern "C" on the definition?

    Reveal the Model Answer

    The C++ compiler mangles the function name. The linker then looks for the plain C name and does not find it. So the build stops with an undefined reference at link time. Add extern "C" to the definition.

    Self-Check

    Tick each key point you covered in your answer.

    Misconception

    "A missing extern \"C\" gives wrong values at run time."

    It fails earlier, at link time, as an undefined reference. Wrong values come from a type mismatch, which the header catches.

    Source Lesson D1.1 key points

  4. ComparePure, Plain and Context ImportsL2 Mid-Level

    Compare a pure import, a plain import and a context import. When do you need each?

    Reveal the Model Answer

    A pure function promises its result depends only on its arguments, with no side effects. So the simulator may skip or reuse calls. A plain import may keep state and have side effects but must not call back into SystemVerilog. A context import may call exported SystemVerilog routines and ask for its calling scope. This costs some optimisation, so use it only where C calls back.

    Self-Check

    Tick each key point you covered in your answer.

    Half-Truth

    "Mark every import pure; it runs faster."

    A function with state, such as a model update, may then be called fewer times than written, and the model drifts.

    Source IEEE 1800-2017 §35.5.2 (pure functions)

  5. What IfPassing a Whole Packet to CL2 Mid-Level

    Asked if they need to pass a whole packet

    What if the C model needs a whole packet of bytes, whose length varies?

    Reveal the Model Answer

    Declare the argument as an open array, input byte unsigned data[], and C receives an svOpenArrayHandle. It asks the handle for the bounds with svSize, svLeft and svRight, and gets element pointers with svGetArrElemPtr1 using SystemVerilog indexes. Those pointers are valid only during the call, so copy anything the model must keep.

    A Handle Describes the ArrayArrows show what each side passes or reads. The handle is a descriptor; the bytes stay in the simulator's storage and C reads them one at a time through the accessor.
    A handle describes the arrayArrows show what each side passes or reads. The handle is a descriptor; the bytes stay in the simulator's storage and C reads them one at a time through the accessor. SystemVerilog main.sv byte unsignedascending[3:5] = '{1,128, 255}; descriptor svOpenArrayHandle dimensions 1 · left 3 ·right 5 · size 3 simulator storage elements [3] = 1 [4] = 128 [5]= 255 C sum_bytes asks the handle, thenreads [3], [4], [5] output total 384, written throughthe output pointer passes a handledescribessvLeft, svRight,svSizesvGetArrElemPtr1(h,i)writes
    1. Pass the handle

      The testbench calls sum_bytes(ascending, 3, total). C gets a handle to the array, with its bounds, and a count of valid bytes.

    2. Ask the shape

      Before reading anything, C asks the handle for its dimension count and bounds: one dimension, left 3, right 5, size 3.

      Follow the calls, values and waits
      1. call svLeft(h, 1): sum_bytes → svOpenArrayHandle
      2. return left 3, right 5, size 3: svOpenArrayHandle → sum_bytes
    3. Walk from the left bound

      Three reads through the accessor, at the array's own indexes 3, 4 and 5. Each pointer is used at once and never kept.

      Follow the calls, values and waits
      1. data 1, 128, 255: elements → sum_bytes
    4. Return the sum

      The sum 1 + 128 + 255 = 384 goes out through the output pointer, and status 0 says it is valid.

      Follow the calls, values and waits
      1. data 384: sum_bytes → total
    Read the diagram as text
    • main.sv: byte unsigned ascending[3:5] = '{1, 128, 255};
    • svOpenArrayHandle: dimensions 1 · left 3 · right 5 · size 3
    • elements: [3] = 1 [4] = 128 [5] = 255
    • sum_bytes: asks the handle, then reads [3], [4], [5]
    • total: 384, written through the output pointer
    • main.sv → svOpenArrayHandle: passes a handle
    • svOpenArrayHandle → elements: describes
    • sum_bytes → svOpenArrayHandle: svLeft, svRight, svSize
    • sum_bytes → elements: svGetArrElemPtr1(h, i)
    • sum_bytes → total: writes

    The handle is not a byte pointer. Ask it for the bounds, then read each element by the index the caller used.

    Self-Check

    Tick each key point you covered in your answer.

    Misconception

    "C gets a plain unsigned char* to element 0 and can index from 0."

    It gets a handle, and indexes follow the SystemVerilog declaration, so an array declared [3:5] has no element 0.

    Source IEEE 1800-2017 §35.5.6.1 (open arrays)

  6. Break ItA C Model That Kept a String PointerL2 Mid-Level

    A C model stores the const char* it receives for a test name and prints it at the end of the test. Sometimes it prints garbage. Why?

    Reveal the Model Answer

    A string argument reaches C as a pointer to memory the simulator owns, valid only while the call runs. After the call returns, the simulator may reuse or free that memory, so the stored pointer can point at anything. Copy the text into storage the C side owns during the call.

    The Life of a String Across the CallArrows show where the pointer points and what C does with it. The text lives in the simulator's buffer; the copy in kept is the only thing that survives the call.
    The life of a string across the callArrows show where the pointer points and what C does with it. The text lives in the simulator's buffer; the copy in `kept` is the only thing that survives the call. SystemVerilog string cfg "AXI" in thesimulator's ownbuffer pointer const char* name points at that bufferwhile the call lasts C protocol_code strcmp during thecall, returns 2 model storage kept[32] remember_name copiesthe bytes; the modelkeeps this SystemVerilog cfg = "changed…" after the call thesimulator reuses itsbuffer passesread nowcopied nowstale pointer
    1. Pass

      The testbench calls protocol_code(cfg). C gets a pointer to the bytes A X I and a terminating zero, nothing more.

      Follow the calls, values and waits
      1. data AXI: string cfg → const char* name
    2. Read during the call

      strcmp reads the bytes and the function returns 2. The pointer has done its job.

      Follow the calls, values and waits
      1. data AXI: const char* name → protocol_code
      2. return 2: protocol_code → const char* name
    3. Copy what you must keep

      remember_name copies the bytes into kept, storage the model owns. After this the model never touches name again.

      Follow the calls, values and waits
      1. data AXI: const char* name → kept[32]
    4. The call returns

      The testbench assigns a new value to cfg. A kept name pointer would now read whatever the simulator put there; kept still says AXI.

    Read the diagram as text
    • string cfg: "AXI" in the simulator's own buffer
    • const char* name: points at that buffer while the call lasts
    • protocol_code: strcmp during the call, returns 2
    • kept[32]: remember_name copies the bytes; the model keeps this
    • cfg = "changed…": after the call the simulator reuses its buffer
    • string cfg → const char* name: passes
    • const char* name → protocol_code: read now
    • const char* name → kept[32]: copied now
    • cfg = "changed…" → const char* name: stale pointer

    Borrow during the call, copy to keep. A pointer that outlives the call points at memory the simulator may have changed, moved or freed.

    Self-Check

    Tick each key point you covered in your answer.

    Misconception

    "The string lives as long as the SystemVerilog variable does."

    The pointer refers to the simulator's copy for that call, not to the variable, so a later change or reuse invalidates it.

    Source IEEE 1800-2017 Annex H.7.2 (argument passing: ownership of string storage)

What People Get Wrong

Common wrong answers and why they fail. Run It shows the wrong belief failing on a real simulator.

Misconception

"byte holds 0 to 255, like a byte in C."

byte is a signed 8-bit type, so 200 reaches C as -56. The twin's model calls it small; byte unsigned fixes it.

Run It

A burst length of 200 passed as byte to a C function that tests len > 127.

RUNVerilator + C/C++ over DPI · simulator service
Edit the code, then run it
Expected: FAIL: byte is signed, so C received -56 and called it small
See the Fix
module tb;
  // The fix: byte unsigned maps to unsigned char.
  import "DPI-C" function int is_large(input byte unsigned len);
  byte unsigned len = 8'd200;
  initial begin
    if (is_large(len)) $display("PASS: C received 200 and called it large");
    else $display("FAIL: byte is signed, so C received -56 and called it small");
    $finish;
  end
endmodule

Prints: PASS: C received 200 and called it large

Tool as Truth

"It linked and ran, so the C prototype matches the import."

DPI-C uses C linkage, which carries no types. Without the generated header, a mismatched prototype links and passes garbage, as lesson D1.1 measured.

Source Lesson D1.1 review claims (a mismatched output argument linked and returned a wrong sum on Verilator 5.052)

Missed Corner

"Pass the bus as logic [7:0] and C gets the x bits as ordinary numbers."

A logic vector crosses as pairs of words called aval and bval. The C side must read bval to see an x or a z. On a two-state simulator the x is already 0 before the call.

Source IEEE 1800-2017 Annex H.8.3 (canonical representation of logic vectors)

Spec Misquote

"The C function can wait a few cycles with a sleep."

An import function returns in zero simulation time. A sleep only stalls the simulator's wall clock; time-consuming behaviour belongs in SystemVerilog tasks.

Source IEEE 1800-2017 §35.5.1

Learn It, Then Prove It