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.
| When | What name points at | What the model printed |
|---|---|---|
During configure("AXI") | the simulator's buffer holding AXI | protocol: AXI |
| After the testbench reassigned the string | the same buffer, now holding other text | protocol: changed after the call |
| After the simulator freed the buffer | memory that belongs to someone else | garbage, 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.
kept is the only thing that survives the call.- Pass
The testbench calls
protocol_code(cfg). C gets a pointer to the bytesA X Iand a terminating zero, nothing more.Follow the calls, values and waits
- data AXI: string cfg → const char* name
- Read during the call
strcmpreads the bytes and the function returns 2. The pointer has done its job.Follow the calls, values and waits
- data AXI: const char* name → protocol_code
- return 2: protocol_code → const char* name
- Copy what you must keep
remember_namecopies the bytes intokept, storage the model owns. After this the model never touchesnameagain.Follow the calls, values and waits
- data AXI: const char* name → kept[32]
- The call returns
The testbench assigns a new value to
cfg. A keptnamepointer would now read whatever the simulator put there;keptstill 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.
| Carrier | Zero bytes | Length | Good for |
|---|---|---|---|
string → const char* | end the text | found by scanning for the terminator | names, modes, file paths, messages |
byte unsigned d[] → svOpenArrayHandle | ordinary values | from the handle, plus a count of valid bytes | packets, 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.
APB=1 AXI=2 empty=-1 apb=-1 remembered=AXIRead 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
Remember
- A
stringargument reaches C asconst 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
stringresult crosses back asconst 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.