Interviews
DPI Interview Questions
Practise the question, then the follow-ups an interviewer asks next.
Start with a Question TreeAI Mock Interview
- 15
- Question trees
- 63
- Follow-ups
- 113
- Wrong answers explained
- 17
- Runnable on a simulator
import "DPI-C" and export "DPI-C"? When do you use a task instead of a function on each side?
WhyWhy an Export Line Has No TypesCompareDPI Compared with VPIWhat IfA C Model That Needs a DUT SignalBreak ItCalling a Class Object from CWhat IfHow C Code Joins the Simulation
Open this tree
Flagship Questions · With follow-ups
Question Trees
Each question opens into the follow-ups an interviewer asks next, the wrong answers people give, and what a fresher's and a senior's answer sound like.
-
Import and Export in DPI
What is the difference betweenimport "DPI-C"andexport "DPI-C"? When do you use a task instead of a function on each side? 5 follow-ups · 8 pitfalls · 1 runnable -
What Context and Pure Promise
What doescontextgive the C code, and what doespureallow the simulator to do? Which do you choose for a stateful reference model? 4 follow-ups · 8 pitfalls · 1 runnable -
Making a C Model Wait for the Design
Your C model must wait until the DUT raisesreadybefore it sends the next item. How do you do that with DPI, and what are the rules? 4 follow-ups · 7 pitfalls · 1 runnable -
Sending a Callback to the Right Instance
Two instances of a module exportsv_accept. Your C function must send one log line to a centralloggerinstance, then callsv_acceptin the instance that called it. Write the C. 4 follow-ups · 7 pitfalls · 1 runnable -
A Simulation That Died in the C Model
A test dies with this output. The log file is empty, so even the scoreboard message printed before the call is missing. Read the backtrace: what is the cause, and what do you do next? 4 follow-ups · 7 pitfalls · 1 runnable -
A Batched DPI Call That Got Slower
A teammate batched a per-byte CRC into one DPI call per packet to save time. The timing log says the batch is slower. Explain the numbers and say what you would change. 4 follow-ups · 7 pitfalls · 1 runnable -
When Not to Use DPI
When would you advise a team against DPI? Give concrete cases. 4 follow-ups · 8 pitfalls · 1 runnable -
A Scoreboard That Calls a C Model
An input monitor and an output monitor both send transactions to your scoreboard. A C model givesmodel_read(h, addr), the data the DUT should return. Write the scoreboard methods: where do you call the model, and what do you compare? 4 follow-ups · 7 pitfalls · 1 runnable -
Running a Python Model from the Testbench
The architecture team gives you a Python model. How do you call it from a SystemVerilog bench, and what goes wrong most often? 5 follow-ups · 9 pitfalls · 1 runnable -
Integrating a C Model into a UVM Bench
Walk me through integrating an existing C reference model into a UVM bench, from the first build to sign-off. 5 follow-ups · 9 pitfalls · 1 runnable -
A Static Variable That Lost Its Value
What does this print? The packet of length 10 carries an inner packet of length 4. 4 follow-ups · 7 pitfalls · 1 runnable -
Who Frees DPI Memory
A C model returns astringbuilt withmallocon every call. Who frees it? And who frees astringargument, an open array, or a model behind achandle? 4 follow-ups · 8 pitfalls · 1 runnable -
Reading a Signed Field in C
What does this print? The SystemVerilog value is -5. 4 follow-ups · 7 pitfalls · 2 runnable -
Walking an Open Array in C
Write the C body forimport "DPI-C" function int checksum(input byte unsigned pkt[], input int count);. It sums the firstcountbytes in the order the caller wrote them, and must work forpkt[0:63],pkt[63:0]andpkt[1:8]. 4 follow-ups · 7 pitfalls · 1 runnable -
A Packet Passed as a String
A colleague passes packet bytes to C as astring. What does this print? 4 follow-ups · 7 pitfalls · 2 runnable
No question matches these filters.
Quick revision · One flat list
Question Bank
All 77 DPI questions, main and follow-up. Open a track, then a topic. Answer out loud first, then open the short answer.
Jump to a Track
77 questions · 15 topicsDPI
Import and Export in DPI6
What is the difference between import "DPI-C" and export "DPI-C"? When do you use a task instead of a function on each side? L0L1
An import lets SystemVerilog call a C routine. An export lets C call a SystemVerilog function or task. The import line gives the C name, the directions and the types. The export line names only the SystemVerilog routine, because its own declaration already has the types. Use functions when C computes and returns at once. Use tasks only when C must call an exported task that waits for simulation time.
QuestionPractise Import and Export in DPI →Why does export "DPI-C" function sv_accept; carry no argument list, when an import must spell out every type? L1
For an export, the SystemVerilog function declaration already holds the types. Repeating them would give two copies that can drift apart. For an import, the C body is invisible to SystemVerilog, so the import line is the only description it has. The simulator writes a C prototype for both into its generated header.
Follow-upPractise Why an Export Line Has No Types →Compare DPI with VPI. Which one do you use for a C reference model, and which one for a tool that browses the design? L1L2
DPI is a typed function call: you pass values in and get values out, like any C call. VPI is an object model of the design. C code finds signals by name, reads and writes them, and registers callbacks on value changes. Use DPI for reference models and helper code. Use VPI for tools such as debuggers, waveform dumpers or coverage readers that must walk the hierarchy.
Follow-upPractise DPI Compared with VPI →What if the C model needs the value of a DUT status register at each prediction? L2
Pass it in. The monitor samples the register and gives it to the import as an argument. If C must ask for it in the middle of a call, export a small getter function and make the import context. Avoid hierarchical lookups from C: they hide what the model depends on, and they break when the hierarchy changes.
What if C needs to call a method of a scoreboard object? Can you export the class method? L2
No. Class methods cannot be exported, because objects do not exist when the design is elaborated. Export a function in a module or package instead. Keep the objects in a table on the SystemVerilog side, and pass C an index or an ID. The exported function looks the object up and calls its method.
Follow-upPractise Calling a Class Object from C →What if someone asks how the C code actually gets into the simulation? Describe the build. L1L2
With Verilator, you list the .c or .cpp files on the command line. They are compiled and linked into the one simulation binary. Many event-driven simulators instead load a shared library at run time, named with a switch such as -sv_lib. Either way, the C must use C linkage, so C++ files need extern "C".
What Context and Pure Promise5
What does context give the C code, and what does pure allow the simulator to do? Which do you choose for a stateful reference model? L1L2L3
A context import gets the calling scope. So C may call exports, use svGetScope and use VPI. The simulator must record that scope and assume C may change SystemVerilog state. A pure function promises its result depends only on its arguments. So the simulator may skip or reuse calls, though it does not have to. A stateful model is never pure: use a plain import, and context only if it calls back.
Why is a context import slower than a plain one? L2
On each call, the simulator records the calling scope so C can ask for it. C may also call exports that change SystemVerilog variables. So the simulator cannot keep values cached across the call. A plain import promises neither, so the call can be cheaper.
Follow-upPractise Why Context Imports Cost More →The C model needs five configuration values. Compare passing them as arguments with letting C fetch them through exported getters. L2L3
Arguments keep the import plain and the data flow visible. C cannot read anything the testbench did not hand it. Getters need a context import and hide the dependency inside C. They also run on every call unless C caches them. Pass configuration once, at model creation or reset, and keep callbacks for real events such as reporting an error.
Break it: a sequence-number model is declared pure, and every test passes on your simulator. What could happen on another simulator or a new release? L3
The other tool may skip a call whose result looks unused, or reuse an earlier result for the same arguments. Then the model misses an update, and its sequence numbers drift from the DUT. Nothing in your passing runs proves the declaration is safe. Review imports for pure on any function with side effects.
How do you stop this mistake across a team of twenty engineers? L3L4
Keep every import in one package, so a reviewer sees all declarations together. Allow pure only for functions that are clearly mathematical, such as a CRC of the arguments. Run the regression on a second simulator when the project has one, because skipped calls show up there first.
Making a C Model Wait for the Design5
Your C model must wait until the DUT raises ready before it sends the next item. How do you do that with DPI, and what are the rules? L2L3
C itself cannot let simulation time pass. The simple way is to let SystemVerilog wait for ready, then call C, which stays untimed. The standard also allows a context import task whose C calls an exported task that waits. That thread then sleeps inside the export while the rest of the simulation runs. Verilator 5.052 rejects timing controls in exported tasks, so there you must use the first way.
Why must both the import and the export be tasks when C needs to wait? L2
The wait itself, such as @(posedge clk), can only live in a task, because functions may not contain timing controls. A function cannot call a task. So the chain from SystemVerilog through C and back must be task, task, task. The import must also be context, because it calls an export.
What if the test runs disable fork on the thread while C is waiting inside the exported task? L3
The exported task returns 1 to C instead of 0, which means the thread was disabled. C must stop at once, free what it holds and call svAckDisabledState. Then the C task returns 1 to the simulator. C must not call any more exports in that state.
Break it: the C code ignores the result of sv_wait_ready() and goes on to send the next item. What goes wrong? L3
C carries on as if the wait finished. It may call more exports for a thread that no longer exists, which the standard does not allow. Buffers it allocated for the item are never freed. The test can end with the model one item ahead of the testbench.
Follow-upPractise Ignoring the Disable Result →Compare a C model that waits through exported tasks with an untimed model that SystemVerilog calls once per transaction. L3
An untimed model is a pure prediction step. It is portable, easy to test alone, and the testbench decides when to call it. A timed model owns its own sequence, which suits legacy C test programs. But it needs context task imports, careful disable handling and reentrant C code. Choose the untimed model unless existing C already drives the flow.
Follow-upPractise Timed C Models Compared with Untimed Ones →Sending a Callback to the Right Instance5
Two instances of a module export sv_accept. Your C function must send one log line to a central logger instance, then call sv_accept in the instance that called it. Write the C. L2L3
Save the caller with svGetScope() as the first step. Switch to the logger with svSetScope, using a scope from svGetScopeFromName("tb.logger"), and call the log export. Then restore the saved scope before calling sv_accept. An export always runs in the current scope, and svSetScope changes it for the rest of the call. Look the name up once and check it for null.
What if the context import is declared in a package, and two module instances call it? Which scope does C see? L3
The context of an import is where it is declared, so a package import has the package as its scope. Verilator 5.052 reported the calling instances, tb.u0 and tb.u1, in our probe. Code that relies on that may break on another simulator. Declare a context import inside the module whose exports it calls.
How can C keep separate state for each module instance without passing a handle on every call? L3
Attach it to the scope. svPutUserData(scope, key, ptr) stores a pointer for that instance, and svGetUserData(scope, key) gets it back. The key is the address of a static variable, so two libraries never collide. Create the state on the first call, when the lookup returns null.
Compare scope user data with a chandle per model. Which one fits a UVM scoreboard? L3
Scope data belongs to a module, interface or program instance. A UVM scoreboard is a class object, and all its calls come from the same import scope. So scope data cannot tell two scoreboards apart. A chandle from model_new() belongs to the object that holds it. Use the chandle for class-based benches.
Break it: the bench is reused one level deeper, and svGetScopeFromName("tb.logger") now finds nothing. What happens? L2
The lookup returns null. If C passes that to svSetScope and calls an export, the export has no scope. Verilator 5.052 stops the run with a "scope wasn't set" error. Check every lookup, and return an error code the testbench reports.
A Simulation That Died in the C Model5
A test dies with this output. The log file is empty, so even the scoreboard message printed before the call is missing. Read the backtrace: what is the cause, and what do you do next? L2
Frame #0 is the C model, model_predict, with h=0x0. Frame #2 shows the scoreboard's write made the call through the DPI wrapper. So the scoreboard's chandle was never set: nothing called model_new before the first prediction. The empty log is stdout buffering, because the process died before it flushed. Fix the build order, and make the C return a status for a null handle.
Why did the $display that ran just before the crash never reach the log? L2
When output goes to a file or a pipe, the C library collects it in a buffer. It writes the buffer in large blocks. A segmentation fault kills the process before that buffer is flushed. Our probe showed the line once $fflush() ran before the risky call. Running under gdb also avoids guessing, because it stops at the crash.
Prove it: how do you get that backtrace from a failing regression run? L2
Build the C model with -g, so frames show file and line. Rerun the failing seed under gdb with run then bt. Or allow core files with ulimit -c unlimited, and open the core with gdb ./obj/Vtb core. Read frame #0 first, then walk down to the first frame in your own code.
What if the crash happens on the compute farm but never on your machine? L3
Suspect undefined behaviour in the C: an uninitialised variable, a stale pointer or a read past an array. It shows up only with certain compilers, flags or memory layouts. Reproduce with the farm's exact build flags and seed, then run an AddressSanitizer build. It reports the bad access where it happens, not where it later crashes.
Follow-upPractise A DPI Crash That Only Happens on the Farm →Compare a C model that returns a status code on bad input with one that calls abort() or exit(). L2
A status lets SystemVerilog report the error through the normal error path, with the time, the transaction and the test name. The run then ends cleanly, and its final blocks and end-of-test reports still run. An abort kills the process at once and skips all of that. So C returns a status, and SystemVerilog decides how bad it is.
Follow-upPractise Status Codes Compared with Aborting in C →A Batched DPI Call That Got Slower5
A teammate batched a per-byte CRC into one DPI call per packet to save time. The timing log says the batch is slower. Explain the numbers and say what you would change. L3L4
The crossing is not the cost here: Verilator turns an import into a direct C++ call. The batched version still calls svGetArrElemPtr1 once per byte, and at -O0 each call does full bounds checks. At -O2 both versions take the same time, so the CRC loop itself dominates. Take one pointer with svGetArrayPtr when it is not null, and build the model optimised. Profile on the sign-off simulator before changing the interface again.
Why is a DPI call so cheap in Verilator? L3
Verilator compiles the whole testbench to C++. An import becomes a small generated wrapper that converts the arguments and calls your function in the same binary. Our backtrace shows that wrapper, Vdpiimwrap_model_predict, as one frame. There is no interpreter between the two sides.
Compare passing logic [511:0], bit [511:0] and a string to C a million times in a loop. L3
In our -O0 probe, a million calls took 46 ms with logic [511:0], 30 ms with bit [511:0] and 50 ms with a short string. A four-state vector carries two words for every 32 bits, so there is more to convert. A string may need a conversion to const char*. In a hot path, prefer two-state types and numbers.
Prove it: how do you show where the time goes before you change the model's interface? L3
Run the simulator's profiler, or perf on a compiled binary, and read the top functions. Count the calls into C per test. Then time the same calls with an empty C body. That time is the crossing, and the rest is the model's own work.
What if the C model is called from an always @(posedge clk) block on every cycle, even when no transfer happens? L2L3
The call count now grows with simulated cycles, not with transactions. A long idle period costs as much as busy traffic. Call the model only when the protocol says a transfer happened, such as valid && ready at the edge.
When Not to Use DPI5
When would you advise a team against DPI? Give concrete cases. L2L3
Avoid DPI for work SystemVerilog already does well: random stimulus, waiting for time, small checks and text formatting. C rand() ignores the SystemVerilog seed and random stability, so a failing seed may not reproduce. DPI also adds a second build and debug flow, and a C bug crashes the whole simulation. Use it when a trusted model already exists in C, C++ or Python, or the algorithm is heavy.
Why does C rand() break seed reproducibility, when the test always calls it in the same place? L2
C rand() is one stream shared by every caller in the process. Its next value depends on how many calls came before, from any thread or any model. Add one call in another component, and every later value moves. SystemVerilog avoids this by giving each thread and object its own generator.
What if the C model genuinely needs random numbers, for example to model random arbitration? L2L3
Give each model its own generator, such as a std::mt19937 inside the model struct. Seed it from a value SystemVerilog passes at creation, taken with $urandom. Then the SystemVerilog seed controls the model, and each model has its own stream.
Compare calling a model through DPI with running the model offline and reading its expected results from a file. L2
A vector file is simple: no shared build, and the model runs once. But the stimulus is fixed in advance, so the test cannot react to random choices, backpressure or resets. DPI keeps the model in the loop for every transaction. Use files for fixed directed suites, and DPI for constrained-random benches.
Follow-upPractise DPI Compared with Precomputed Vector Files →Break it: the C++ model throws std::out_of_range on a bad address, and nothing catches it. What happens? L2
The exception tries to unwind through simulator code that does not expect it. The standard calls that undefined behaviour. In practice the process usually stops in std::terminate with no test report. Wrap every import body in try/catch and return a status instead.
A Scoreboard That Calls a C Model5
An input monitor and an output monitor both send transactions to your scoreboard. A C model gives model_read(h, addr), the data the DUT should return. Write the scoreboard methods: where do you call the model, and what do you compare? L2
Call the model when the input monitor reports an accepted request, using that request's fields. Push the prediction into an expected queue. When the output monitor reports a response, pop the oldest prediction and compare. Never predict from the response itself: if the DUT corrupts the echoed address, the model agrees with the bug. Create the handle in the constructor or build phase, and check the queue is empty at the end.
QuestionPractise A Scoreboard That Calls a C Model →What if the DUT may return responses out of order, matched by an ID? L2L3
A single queue would compare the wrong pairs. Store predictions in an associative array keyed by ID, each holding a queue for repeated IDs. On a response, look up its ID, pop the oldest prediction for it, and compare. At the end, every queue must be empty.
Follow-upPractise A C Model with Out-of-Order Responses →The architecture team owns a C++ model of the whole subsystem. How do you use it as the scoreboard's reference without forking it? L3
Wrap it behind a thin, versioned DPI layer owned by verification: transaction in, predicted transactions out, one handle per instance, plus reset. Feed it from monitors, never drivers, so it predicts from what the DUT actually received. Agree on where the model is untimed, such as ordering between independent streams, and compare there by key rather than by cycle.
Follow-upPractise Integrating an Architecture Model at Subsystem Level →Compare a C reference model behind DPI with a reference model written as a SystemVerilog class. L2
A C model can be the architecture team's own code, so verification checks against an independent source. It costs a second language, a boundary and a second debug flow. A SystemVerilog model debugs in one tool and sees four-state values. Choose C when the model already exists or the algorithm is heavy; otherwise keep it in SystemVerilog.
Follow-upPractise A C Model Compared with a SystemVerilog Model →Prove it: how do you show that this scoreboard would catch a wrong DUT response? L2
Break something on purpose and watch it fail. Corrupt one DUT output with a force, or add a bug in a scratch copy of the RTL. Then check that the scoreboard reports exactly the expected mismatch. Also count predictions and responses: equal counts show the model saw every transaction.
Follow-upPractise Showing the Scoreboard Can Fail →Running a Python Model from the Testbench6
The architecture team gives you a Python model. How do you call it from a SystemVerilog bench, and what goes wrong most often? L2L3
DPI reaches only C, so a C bridge embeds CPython in the simulation. Start the interpreter once, import the module once, and keep the function object. Copy each transaction into Python objects, and check every conversion back. Turn every Python exception into a status code. Give the model an explicit reset, because importing a module again returns the cached copy with its old state.
QuestionPractise Running a Python Model from the Testbench →Why does a second PyImport_ImportModule("model") not reset the model? L2
Python runs a module's top-level code only on the first import. It then stores the module in sys.modules, and every later import returns that object. So total keeps its value. A reset must be a function the model provides.
What if the reference model is written in Python? How does it reach the testbench? L2L3
The standard defines DPI only between SystemVerilog and C. So a small C bridge starts the Python interpreter, calls the model and converts arguments and results. The bridge must catch every Python exception and turn it into a status code, because nothing may unwind into the simulator. Pass a whole packet per call, not one byte, because each call into Python is much slower than a C call.
Follow-upPractise A Python Model Behind DPI →Compare the cost of calling a Python model per byte with calling it per packet. L2
In our probe, 640000 per-byte calls into Python took 32 ms. 10000 per-packet calls took 6 ms for the same bytes. Each call crosses into the interpreter and builds Python objects, so fewer, larger calls win. The real cost is usually the Python work inside the model, so measure before redesigning.
Follow-upPractise Python Calls Compared with C Calls →How do you make the Python bridge behave the same on every farm machine and in every regression? L3
Start CPython with an isolated configuration, so environment variables and user site packages are ignored. Put only the model's own directory on sys.path. Pin the interpreter version the bridge was built against, and print the model version at start of test. Then a failure on one machine can be reproduced on another.
Break it: the bridge starts Python with default settings. A user presses Ctrl-C to stop a hung simulation, and nothing happens. Why? L3
By default, Python installs its own handler for SIGINT. The handler only sets a flag that Python checks while it runs Python code. While the simulator runs outside Python, nobody checks that flag. Set install_signal_handlers = 0, so Ctrl-C stays with the simulator.
Integrating a C Model into a UVM Bench5
Walk me through integrating an existing C reference model into a UVM bench, from the first build to sign-off. L3L4
First agree the C interface with the model owners: transaction in, prediction out, a handle per instance, reset, delete and status codes. Put every import in one package behind a thin wrapper that verification owns. A predictor calls the model for each accepted input transaction and sends predictions to the scoreboard. Reset the model with the DUT, and fail on predictions left at the end. Sign off only after a known DUT bug makes the scoreboard fail.
QuestionPractise Integrating a C Model into a UVM Bench →Why put every DPI import in one package instead of next to the code that calls it? L3
The import lines are the contract with C. In one place, a reviewer checks every type, pure and context together. A model change edits one file, and the build fails at once wherever a caller disagrees. It also gives one place to swap in a stub model for unit tests.
What if the C model is untimed, but the DUT has latency, backpressure and a pipeline? L3
Let time stay in the bench. The model predicts once per accepted input, in zero time, and the prediction waits in the scoreboard. The comparison happens when the output monitor reports, so latency does not matter. Where the model and the DUT may legally order things differently, compare by key, not by cycle.
Follow-upPractise An Untimed Model Against a Timed DUT →Prove it: what evidence do you need before you call the C model integration done? L3
A test with a known DUT bug must fail in the scoreboard with the right message. Prediction and response counts must match in every passing test, with nothing left at the end. A sanitizer build of the C must run clean on a subset. The model version must be printed in every log.
Follow-upPractise Sign-Off for a C Model Integration →Break it: the model writer and the RTL designer read the same unclear spec line the same wrong way. What catches it? L3L4
The scoreboard does not, because model and DUT agree. Catch it with checks that someone else writes straight from the spec. Use assertions, directed tests with hand-worked values, and spec reviews of the unclear lines. Log the question to the spec owner, and fix both sides.
Follow-upPractise When the Model and the DUT Share a Bug →A Static Variable That Lost Its Value5
What does this print? The packet of length 10 carries an inner packet of length 4. L2L3
It prints 4, not 10. The export runs inside the outer call and calls c_parse again for the inner packet. That nested call writes the one static cur_len, so the outer call returns the inner length. A static is safe only if nothing can re-enter before the call finishes. A callback can re-enter, and so can a context task that waits in an exported task. Keep in-progress state in local variables or a per-call struct.
Why is a C static safe when two forked SystemVerilog threads call the same plain import? L2
An imported function returns in zero time and cannot block. So the scheduler never switches threads while it runs. The second thread enters only after the first call has returned. The danger starts only when C calls back into SystemVerilog, or when an imported task suspends.
Follow-upPractise Why Forked Threads Do Not Interleave in C →Compare the three places C state can live: a local variable, a struct behind a handle, and a static or global. L2
A local lasts for one call, so re-entry cannot harm it. A struct behind a chandle lasts as long as the model, and each instance owns its own. A static lasts for the whole simulation and is shared by every caller and every instance. Use locals for work in progress, the struct for model state, and statics almost never.
What if the import is a context task that waits inside an exported task, and two agents call it? L3
Agent A enters C and suspends inside the export. While it waits, agent B calls the same C function and runs it from the top. Both calls are now live in C at once. Any static they touch is shared, so the C code must be reentrant.
Follow-upPractise Re-Entry Through a Suspended Task →Break it: someone wraps the C task in a std::mutex to fix the re-entry. What happens? L3
Agent B's call comes from the same OS thread that still holds the lock for agent A. Locking a std::mutex you already own is undefined, and usually blocks forever. Agent A can never resume, because the simulator thread is now stuck. Remove the static instead.
Who Frees DPI Memory5
A C model returns a string built with malloc on every call. Who frees it? And who frees a string argument, an open array, or a model behind a chandle? L2L3
Each side frees only what it allocated. Strings, arrays and vectors that SystemVerilog passes in are borrowed for the call. C never frees them or keeps pointers to them. A string C returns is still C's memory. The simulator copies it and never frees it, so a fresh malloc per call leaks. Keep result buffers inside the model, and give a chandle model a delete call that SystemVerilog makes once.
Why does the standard forbid C from freeing memory that SystemVerilog passed in, even if it came from malloc? L2
C cannot know how the simulator made that memory. It may come from a pool, the stack, a temporary copy, or a C++ object. Only the allocator that made a block may free it. A clear rule on each side makes both implementations free to change.
Follow-upPractise Why Each Side Frees Its Own Memory →Compare a fresh malloc per call, one static buffer, and a buffer inside the model struct for a string result. L2
The fresh malloc leaks on every call. The static buffer does not leak, but every model shares it and the next call overwrites it. A buffer inside the model struct is owned by that model and freed with it. Choose the model buffer.
Break it: the scoreboard calls model_delete(h) in its final phase, but a late monitor callback still calls model_predict(h, ...). What happens? L2
The chandle still holds the old address, so C writes into freed memory. That may work, return garbage or crash much later. C cannot tell a freed pointer from a live one. Set the handle to null right after delete, and have SystemVerilog check it before every call.
How do you catch leaks and use-after-free bugs in DPI code across a whole regression? L3
Run a nightly subset with AddressSanitizer: build the C model and the simulation with -fsanitize=address. It reports a use-after-free or a wild read at the line that does it. Also keep a live-model counter in C, and check at the end of every test that it is zero.
Reading a Signed Field in C5
What does this print? The SystemVerilog value is -5. L1L2
It prints 4091, not -5. A packed vector such as bit signed [11:0] crosses as an array of 32-bit svBitVecVal words. The 12 bits sit at the bottom of word 0, and nothing copies the sign bit upward. So C reads 0xFFB as a positive number. Sign-extend from bit 11 in C, or use shortint or int, which cross as C short and int with their sign.
Why does an input bit [7:0] arrive as const svBitVecVal*, while byte unsigned arrives as a plain unsigned char? L2
A packed vector can have any width. So the standard gives every vector one form: an array of 32-bit words, passed by reference. byte, int and longint have a fixed size, so each has its own C type and travels by value. The generated header shows which form each argument got.
What if the bus may carry x or z? How does C see them? L2
Each 32 bits of a logic vector cross as one svLogicVecVal with two words. A bval bit of 0 means the bit is known, and aval holds its value. A bval bit of 1 means unknown: aval 1 is x and aval 0 is z. A checker that rejects unknowns must test bval, not aval.
Your main simulator is two-state, but the C checker must reject x. How do you verify that path? L3
Test the C function on its own, with a small C test that builds svLogicVecVal values with bval bits set. Run the DPI x tests on a four-state simulator, if the project has one. Where unknowns matter in the design, also carry an explicit valid mask, which works on both kinds of simulator.
Compare passing a 64-bit value as longint with passing it as bit [63:0]. L1L2
A longint crosses by value as a C long long, ready to use. A bit [63:0] crosses as two svBitVecVal words. Word 0 holds bits 31:0 and word 1 holds bits 63:32, so C must join them with a shift. Prefer longint for numbers, and vectors for fields wider than 64 bits.
Walking an Open Array in C5
Write the C body for import "DPI-C" function int checksum(input byte unsigned pkt[], input int count);. It sums the first count bytes in the order the caller wrote them, and must work for pkt[0:63], pkt[63:0] and pkt[1:8]. L2
C receives an svOpenArrayHandle, not a byte pointer. Check count against svSize, and find the direction from svLeft and svRight. Then step from the left bound and read each element by its SystemVerilog index with svGetArrElemPtr1. Check each pointer for null. Never assume index 0 exists or comes first: in pkt[63:0] the first byte written is pkt[63].
Why does DPI give C a handle for an open array instead of a plain pointer? L2
The simulator's array may have any bounds, packed elements, four-state values or a layout C does not know. The handle hides that layout behind query and access functions. svGetArrayPtr returns a direct pointer only when the layout matches C, and null otherwise.
Compare input byte unsigned d[] with input byte unsigned d[64] as a DPI formal. L2
A sized formal crosses as a plain pointer; the generated header gave const unsigned char* d. Every actual must then have that exact size. An open formal crosses as a handle, so one C function serves any length and any bounds. Use the sized form only when the length is fixed by the protocol.
What if the C model must return a response whose length it only knows after computing it? L2
C cannot grow a SystemVerilog array. The caller passes an output open array big enough for the longest response. C writes up to svSize elements and returns the count it used. If the response does not fit, C returns an error status instead of writing past the end.
Break it: the C model saves the pointer from svGetArrElemPtr1 and reads the packet again in the next call. What goes wrong? L2
The pointer is valid only during the call that produced it. Afterwards, the simulator may move, reuse or free that storage. The next read can return another packet's data or crash. Copy the bytes into C-owned storage before the call returns.
Follow-upPractise Keeping an Element Pointer After the Call →A Packet Passed as a String5
A colleague passes packet bytes to C as a string. What does this print? L1L2
It prints 2, not 3. A SystemVerilog string cannot hold a zero byte, so the 0x00 is dropped as the string is built. Even if it got through, C's strlen stops at the first zero. A string crosses as a borrowed const char* and is for text, such as names and file paths. Send binary data as a byte array with a count.
Why can a SystemVerilog string not hold a zero byte? L1
The standard says so: a string never contains the 0 character, and writing 0 into one character is ignored. That keeps it compatible with C strings, which end at the first zero.
Follow-upPractise Why a String Has No Zero Byte →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? L2
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.
Follow-upPractise A C Model That Kept a String Pointer →What if a model class holds a chandle, and someone copies the object with new this or a copy() method? L2
The copy gets the same address, so both objects drive one C model. An update through either is seen by both. If the copy needs its own state, call a C clone function that allocates a new struct. Then decide which object deletes which model.
Follow-upPractise Copying an Object That Holds a Chandle →Why store the C pointer in a chandle rather than in a longint? L2
A chandle always has the size of a pointer on the machine that runs the simulation. The language only lets you assign it, compare it with null and pass it to C. That stops SystemVerilog code from doing arithmetic on it or inventing values. A longint would work by accident on a 64-bit host and offer none of those guarantees.
Your Review Queue
Saved review reminders are coming soon.