Strings cross as borrowed pointers

Hand a configuration string to C, read it while the call lasts, copy it if the model must keep it, and know when text is the wrong carrier for a packet.

Lesson
D1.5
Time
20 min
Runs on
Verilator with C/C++
Check
5 questions

A bus model takes its protocol name once, at configuration time, and keeps the pointer for its end-of-test report. The report prints protocol: changed after the call on one machine and a row of question marks on another. The pointer was borrowed for one call, and the simulator reused that memory the moment the call returned. This lesson shows how a string crosses into C, how long it lives there, and when text is the wrong carrier.

WhenWhat name points atWhat the model printed
During configure("AXI")the simulator's buffer holding AXIprotocol: AXI
After the testbench reassigned the stringthe same buffer, now holding other textprotocol: changed after the call
After the simulator freed the buffermemory that belongs to someone elsegarbage, or a crash

One pointer, three moments. Only the first is safe, and the model has no way to tell them apart.

Read it during the call

A string formal reaches C as const char*: a pointer to a zero-terminated copy of the text that the simulator owns. protocol_code compares it with strcmp at once and returns a code. It neither frees the pointer, nor writes through it, nor stores it for a later call. Those three rules cover almost every string that crosses into C.

The lookup matches bytes exactly, on purpose. APB gives 1 and AXI gives 2; apb, AXI with a trailing space and the empty string all give -1. A configuration typo that silently selected a default bus would be a bug the testbench cannot see. A -1 is a bug the testbench can assert on.

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.

Copy what you need, and hand back what you own

The example keeps a protocol name across calls with two small functions. remember_name copies the bytes into kept, a static buffer in the bridge, with strncpy and a terminator of its own. remembered_name is declared in SystemVerilog as function string, so C returns const char*, and the simulator copies the text into a SystemVerilog string when the call returns. The testbench reassigns cfg between the two calls, and the report still says AXI.

Return a pointer to a local

extern "C" const char* report() {
  char text[32];
  std::snprintf(text, sizeof text, "proto %d", code);
  return text;   // dies when report() returns
}

The array lives on the stack of report. By the time the simulator copies the text, the function has returned and the memory is gone.

Return a pointer the model keeps

static char text[32];
extern "C" const char* report() {
  std::snprintf(text, sizeof text, "proto %d", code);
  return text;   // valid until the next report()
}

The buffer belongs to the model. The simulator copies it during the call; the next call may overwrite it, which is fine.

Start with numeric status codes and let SystemVerilog print the words: a status is easier to assert on than a sentence. When the model must speak, give it a buffer it owns and say how long the text stays valid.

Text stops at the first zero

C string functions read up to the first zero byte. A packet can contain zero bytes anywhere, so a packet that travels as text loses everything after its first zero. Give binary data a byte array and a count, as sum_bytes does in D1.4. The length travels with the bytes, and a zero is just another value.

CarrierZero bytesLengthGood for
string → const char*end the textfound by scanning for the terminatornames, modes, file paths, messages
byte unsigned d[] → svOpenArrayHandleordinary valuesfrom the handle, plus a count of valid bytespackets, payloads, anything binary

Two carriers for two kinds of data. A string is text; a packet is bytes.

Text brings one more question: the encoding. A SystemVerilog string holds bytes, and one character of a name written in anything but plain ASCII may take several of them. Decide the encoding once for every string that crosses, and compare bytes, never characters, on the C side.

Try it

Run it, then add two assertions: protocol_code("SPI") and protocol_code("Apb") both give -1, because the lookup knows two names and matches their bytes exactly.

Then move the cfg = "changed after the call" line above remember_name(cfg) and watch the second line change: the model copied whatever the string held at the moment of the call.

RUNVerilator + C/C++ over DPI · simulator service
Edit the code, then run it
Expected: APB=1 AXI=2 empty=-1 apb=-1 remembered=AXI
Read included file: bridge.cpp
#include "Vtb__Dpi.h"
#include "svdpi.h"
#include <cstdint>
#include <climits>
#include <cstring>
// A borrowed string: compare it during the call; never free it, never keep the pointer.
extern "C" int protocol_code(const char* name) {
  if (!name) return -1;
  if (std::strcmp(name, "APB") == 0) return 1;
  if (std::strcmp(name, "AXI") == 0) return 2;
  return -1;   // unknown, empty or differently cased: no silent default
}
// Storage the model keeps: the copy outlives the call, so the pointer handed back stays valid.
static char kept[32];
extern "C" void remember_name(const char* name) {
  std::strncpy(kept, name ? name : "", sizeof kept - 1);
  kept[sizeof kept - 1] = '\0';
}
extern "C" const char* remembered_name() { return kept; }   // a string result crosses as const char*

Check yourself

Predict the outputThe example's protocol_code is called with "AXI", "Axi" and "AXI " (a trailing space). What line does the program print?
`timescale 1ns/1ps
module tb;
  import "DPI-C" function int protocol_code(input string name);
  initial begin
    $display("%0d %0d %0d", protocol_code("AXI"), protocol_code("Axi"), protocol_code("AXI "));
    $finish;
  end
endmodule
Read included file: bridge.cpp
#include "Vtb__Dpi.h"
#include "svdpi.h"
#include <cstdint>
#include <climits>
#include <cstring>
// A borrowed string: compare it during the call; never free it, never keep the pointer.
extern "C" int protocol_code(const char* name) {
  if (!name) return -1;
  if (std::strcmp(name, "APB") == 0) return 1;
  if (std::strcmp(name, "AXI") == 0) return 2;
  return -1;   // unknown, empty or differently cased: no silent default
}
// Storage the model keeps: the copy outlives the call, so the pointer handed back stays valid.
static char kept[32];
extern "C" void remember_name(const char* name) {
  std::strncpy(kept, name ? name : "", sizeof kept - 1);
  kept[sizeof kept - 1] = '\0';
}
extern "C" const char* remembered_name() { return kept; }   // a string result crosses as const char*
ConceptMay the C function free the pointer it received for name, once it has read the text?
ConceptA packet may contain bytes of value zero. Which hand-off carries it to C intact?
ConceptA C model hands a short diagnostic back as a string result. Which pointer may it return?
Spot the bugEach option sets up a name and asks protocol_code for its code, and the check expects 2 for AXI. Which option prints FAIL?
Read included file: bridge.cpp
#include "Vtb__Dpi.h"
#include "svdpi.h"
#include <cstdint>
#include <climits>
#include <cstring>
// A borrowed string: compare it during the call; never free it, never keep the pointer.
extern "C" int protocol_code(const char* name) {
  if (!name) return -1;
  if (std::strcmp(name, "APB") == 0) return 1;
  if (std::strcmp(name, "AXI") == 0) return 2;
  return -1;   // unknown, empty or differently cased: no silent default
}
// Storage the model keeps: the copy outlives the call, so the pointer handed back stays valid.
static char kept[32];
extern "C" void remember_name(const char* name) {
  std::strncpy(kept, name ? name : "", sizeof kept - 1);
  kept[sizeof kept - 1] = '\0';
}
extern "C" const char* remembered_name() { return kept; }   // a string result crosses as const char*

Remember

  • A string argument reaches C as const char* to memory the simulator owns. Read it during the call; never free it, write through it or keep the pointer.
  • If the model needs the text later, copy the bytes into storage of its own before the call returns.
  • A string result crosses back as const char*; return a pointer the model keeps, never a local array.
  • Match names exactly and answer an unknown name with an error code, so a typo fails loudly instead of picking a default.
  • Text ends at the first zero byte. Binary data takes a byte array with a count, never a string.