Skip to content
VLSI Mentor

C Part II · Module 19 · DPI-C Foundations

Writing Your First DPI-C Function

Assembling one import end to end turns out to need less interface code than expected and more discipline about data than most introductions suggest. This chapter builds a CRC-8 import — chosen because its four variants and their check values were already settled two chapters ago, so the oracle question stays separate from the interface question — and then does the thing the usual tutorial skips: tests the whole C side before a simulator is anywhere near it, which catches a length bug in milliseconds that a simulation would have reported only as a wrong checksum.

Advanced21 min readCPart IIVerificationDPI-C

Part II · Module 19 · Chapter 19.4 · Writing Your First DPI-C Function

1. What We Are Building

A design computes a CRC-8 over a packet and the testbench needs an expected value. Chapter 18.2 established the independence question, Chapter 18.3 established which CRC-8 — there are at least four and they give 0x52, 0x34, 0xE3 and 0x07 for the same three bytes — and this chapter builds the import.

The algorithm is deliberately one whose correctness is already settled. Measured in Chapter 18.3: this variant is CRC-8/SMBUS, polynomial 0x07, initial value 0x00, no reflection, no final XOR, and its published check value over "123456789" is 0xF4. Reusing it keeps the two questions separate: this chapter is about the interface, and borrowing an algorithm whose correctness has been measured means any failure here is an interface failure.

2. The C

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#include <stdint.h>

/* CRC-8/SMBUS: poly 0x07, init 0x00, no reflection, no final XOR.
   Check value for "123456789" is 0xF4 (catalogue). */
uint8_t c_crc8(const uint8_t *data, int len) {
    unsigned c = 0u;
    for (int i = 0; i < len; i++) {
        c ^= (unsigned)data[i];
        for (int b = 0; b < 8; b++)
            c = (c & 0x80u) ? (((c << 1) ^ 0x07u) & 0xFFu)
                            : ((c << 1) & 0xFFu);
    }
    return (uint8_t)c;
}

That is the complete C side of the import. Measured: it compiles with -Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion and runs clean under AddressSanitizer and UndefinedBehaviorSanitizer.

Three things about it are worth naming, all of them from earlier modules rather than from DPI.

No DPI header, no marking. Chapter 19.2 §3 established this and it holds here: the function has no idea a simulator exists.

The accumulator is unsigned, not uint8_t. Chapter 15.5 §3 measured why — narrow types are promoted before any operator sees them, so doing the arithmetic in unsigned and masking deliberately is the form that says what it means. The & 0xFFu after each shift is the eight-bit wrap, written where it happens.

The length is a parameter. Chapter 13.11 §6 made carrying a pointer with its length one of two habits that remove a bug class, and Chapter 14.7 built the discipline. §5 measures what happens when the length is wrong.

3. The Declaration

From the LRM — this is the part that cannot be executed on this machine, and Chapter 19.1 §4 verified why:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import "DPI-C" function byte unsigned c_crc8(
    input byte unsigned data[],
    input int           len
);

The declaration is what fixes the mapping. SystemVerilog's byte unsigned corresponds to C's uint8_t, int to C's int — Chapter 19.1 §3 measured sizeof(int) as 4, which is what that requires — and an array argument arrives on the C side as a pointer.

Notice that the declaration carries the length as a separate argument. SystemVerilog knows how long its own array is; C does not, and nothing in the interface tells it. That is Chapter 14.7 §2's rule arriving at a language boundary: the length does not travel with the pointer unless somebody sends it.

4. Testing It Before Any Simulator

This is the step the usual introduction skips, and Chapter 19.2 §5 established that it is available.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#include <stdio.h>
#include <string.h>
#include <stdint.h>

uint8_t c_crc8(const uint8_t *data, int len);

static int check(const char *name, const uint8_t *d, int n, uint8_t expect) {
    uint8_t got = c_crc8(d, n);
    printf("  %-22s got 0x%02X  expect 0x%02X  %s\n",
           name, got, expect, got == expect ? "ok" : "FAIL");
    return got == expect;
}

int main(void) {
    static const uint8_t chk[] = {'1','2','3','4','5','6','7','8','9'};
    static const uint8_t abc[] = {'A','B','C'};
    static const uint8_t zero[] = {0x00};
    int ok = 1;
    ok &= check("catalogue vector",    chk,  9, 0xF4);
    ok &= check("\"ABC\"",             abc,  3, 0x52);
    ok &= check("single zero byte",    zero, 1, 0x00);
    ok &= check("empty input",         chk,  0, 0x00);
    printf("%s\n", ok ? "all vectors pass" : "FAILURES");
    return ok ? 0 : 1;
}

Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  catalogue vector       got 0xF4  expect 0xF4  ok
  "ABC"                  got 0x52  expect 0x52  ok
  single zero byte       got 0x00  expect 0x00  ok
  empty input            got 0x00  expect 0x00  ok
all vectors pass

Four vectors, not one — Chapter 18.3 §3 measured why one is not enough, with two CRC-8 variants agreeing on the catalogue value and differing elsewhere.

And the "ABC" result is a genuine cross-check rather than a repetition. Chapter 18.5 §3 ran the identical function inside a running simulation through VPI and got 82, which is 0x52. The same function gives the same answer in both environments, which is the property Chapter 18.5 §3 argued is what you actually need from an interface.

5. What Standalone Testing Catches

The length is the interesting failure, because nothing in the interface protects it.

Suppose the SystemVerilog side passes a 3-byte array but the length argument says 8 — a transposition, a stale constant, a field that was widened. The C does exactly what it was told:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
static const uint8_t abc[] = {'A','B','C'};
(void)c_crc8(abc, 8);          /* len is wrong */

Measured, built with -fsanitize=address:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ERROR: AddressSanitizer: global-buffer-overflow on address 0x0001025a89e3
READ of size 1 at 0x0001025a89e3 thread T0
    #0 0x0001025a882c in c_crc8 crc.c:5

AddressSanitizer names the function and the line. Chapter 13.10 established the tool and Chapter 13.6 established the bug class, and both apply here with nothing new.

What makes this worth a section is the contrast. Without the sanitizer the same program produces a value and exits cleanly — measured, it returns 40 — reading three bytes of array and five of whatever follows, and computing a checksum over all eight.

In a simulation that is what you get: no fault, no diagnostic, a plausible-looking CRC that is simply wrong. The scoreboard reports a mismatch between the design and the model, and the obvious conclusion is that the design is wrong.

So the standalone test is not a convenience:

A data bug inside a DPI import is an ordinary C bug, and the ordinary C tools find it. Inside a simulation it is a wrong answer.

6. The Order to Build It In

Four steps, and the first three need no simulator:

One — write the C and test it against its own vectors. §4. Several vectors, from the specification where possible, run under sanitizers.

Two — build the shared object and check the symbols. Chapter 19.6's subject. Measured: the object exports _c_crc8, and the same source compiled as C++ would export __Z6c_crc8PKhi, which no simulator will find.

Three — decide the contract for the edges. What happens for a zero length, a null pointer, a length longer than the array. Write it next to the declaration, as Chapter 16.8 §7 argued and Chapter 19.3 §3's callout applied to task outputs.

Four — write the SystemVerilog declaration and run it. Last, because by now the only thing that can be wrong is the declaration itself, and a declaration mismatch announces itself loudly — a link error, or a value that is wrong in a structured way.

That ordering inverts the usual one, and the reason is in §5: steps one to three catch the bugs that are silent in a simulation, and step four catches the bugs that are loud.

7. The Misconception: Start With a Trivial Function to Prove the Plumbing

"Get int add(int, int) working through DPI first, to prove the build and the declaration, then write the real function."

It is a reasonable instinct and it tests the wrong thing.

The plumbing almost always works. A declaration, a shared object, a flag telling the simulator where it is. When it is wrong it fails loudly — an unresolved symbol, or a tool error naming the function. Chapter 19.6's mangling hazard is the main way it goes wrong quietly, and that is checkable with nm in one command, without a simulation.

What goes wrong is the data, and int add(int, int) exercises none of it: no arrays, no lengths, no widths, no signedness, no four-state values, and no question about which variant of an algorithm you implemented. Chapter 18.3 measured four implementations of "CRC-8" producing four different answers; a trivial adder would have proved the plumbing perfectly while that was wrong.

The more useful first step is the one §6 describes: write the real function and test it standalone. You will find the data bugs with the ordinary tools, in milliseconds, with line numbers — and the plumbing will tell you about itself when you get to it.

If you do want a smoke test of the interface, make it the real function with a known vector:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
initial begin
  byte unsigned v[] = '{"1","2","3","4","5","6","7","8","9"};
  if (c_crc8(v, 9) !== 8'hF4) $fatal(1, "DPI CRC-8 self-check failed");
end

That proves the plumbing and that the array crossed correctly and that it is the right variant — three things, in one check, using a vector the catalogue publishes.

8. What You Should Be Able to Say Now

  1. The C side is the whole C side. No DPI header, no marking — Chapter 19.2 §3's property, applied.
  2. Use unsigned for the accumulator and mask deliberately — Chapter 15.5 §3's promotion rule, not a DPI concern.
  3. The length is a separate argument, because the interface does not carry it. Chapter 14.7 §2's rule at a language boundary.
  4. Test against several vectors before any simulator. Measured: 0xF4, 0x52, 0x00 and 0x00 for four inputs — and one vector is not enough (18.3 §3).
  5. Cross-check across environments. "ABC" gives 0x52 standalone and gave 82 from inside a running simulation via VPI (18.5 §3).
  6. A wrong length is an ordinary C bug. Measured: ASan reports global-buffer-overflow and names the line. In a simulation the same bug is a wrong CRC and looks like a design failure.
  7. Build in this order: test the C · check the symbols · decide the edge contract · write the declaration. The first three catch the silent bugs; the last catches loud ones.
  8. Keep the self-test in the repository. Milliseconds, full input space, and it keeps the model independently checkable (18.2).
  9. A trivial first function proves the plumbing and tests no data. The plumbing rarely fails quietly; nm checks the one way it does.
  10. A better smoke test is the real function with a catalogue vector — plumbing, array crossing and variant, in one check.

9. Exercises

  1. Build §2's C and §4's test and confirm all four vectors. Then add a fifth of your own choosing and predict its value before running.
  2. Reproduce §5's length bug under -fsanitize=address and record the diagnostic. Then run the same program without the sanitizer and say what you observe.
  3. Write the edge contract for c_crc8: zero length, a null pointer, and a length exceeding the array. Put it next to the declaration.
  4. Change the polynomial to 0x31 and rerun §4's vectors. Which ones change, and what does that tell you about the value of the catalogue vector alone?
  5. Build the shared object and list its symbols. Then build it as C++ and compare. Which would a simulator find?
  6. Write §7's SystemVerilog smoke test for a different algorithm you use, including the vector you would check against and where it came from.
  7. Your model and the design disagree on one packet. Describe how §6's build order makes that diagnosable, and what you would run first.

10. Summary

  • One import, end to end, using an algorithm whose correctness was already settled (18.3) so that any failure here is an interface failure.
  • The C is ordinary C — no header, no marking; unsigned accumulator and deliberate masking (15.5).
  • The length is a separate argument — the interface does not carry it (14.7).
  • Test several vectors with no simulator. Measured 0xF4 · 0x52 · 0x00 · 0x00. One vector is not enough (18.3 §3).
  • Cross-environment check: "ABC" = 0x52 standalone, 82 inside a simulation via VPI (18.5).
  • A wrong length is an ordinary C bug — measured, ASan names the line. In a simulation it is a wrong CRC that looks like a design failure.
  • Order: test the C · check symbols · decide the edge contract · write the declaration.
  • Keep the self-test in CI — milliseconds, whole input space, keeps the model interrogable (18.2).
  • A trivial first function tests no data. Smoke-test with the real function and a catalogue vector.

The declaration in §3 said import "DPI-C" function and nothing more, and there are two qualifiers it could have carried. One of them is a promise about the C that lets the simulator skip calls entirely; the other is what a function needs before it may call back the other way at all.

Standards & specifications

Governing standard
IEEE Std 1800 (SystemVerilog)(opens IEEE in a new tab)

Defines SystemVerilog language semantics — syntax, data types, scheduling and the behaviour a conforming simulator must produce. It does not define tool-specific synthesis support or vendor methodology.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C Programming curriculum.