import "BDPI" gives BSV simulation code a typed function whose
implementation lives in C. It is how you connect reference models, testbench
utilities, file-backed services, and existing software algorithms to a BSV
simulation. The BSV declaration decides which calls type-check; the C
prototype must match the bit carriers that bsc chooses, and nothing compares
the two for you. This chapter follows §16 "Embedding C in a BSV Design" of the
BSV Language Reference Guide: the declaration and build steps, the C carrier
that each packed width selects, Bits types as raw bit patterns, wide and
polymorphic values by pointer, and value, Action, and ActionValue return
shapes.
Before you start. This chapter follows
Embedding RTL in a BSV design. You need
Bits and pack, provisos such as Add, and valueOf from
Typeclasses and provisos, and the
Action and ActionValue shapes from
Methods: value, Action, ActionValue. The previous chapter's question
carries over: which facts does BSV check, and which must you trust across the
foreign boundary?
Note. BDPI is a simulation boundary. A design that calls an imported C function is not ordinary synthesizable hardware. Keep foreign calls outside the synthesizable datapath, and describe hardware behavior in BSV or RTL.
1. The BDPI declaration
1.1 Declaring an imported function
A BDPI declaration looks like a function prototype with no body. An optional link name separates the identifier BSV code calls from the symbol C exports.
Syntax
import "BDPI" function ResultType name(ArgType arg, ...);
import "BDPI" c_symbol = function ResultType name(ArgType arg, ...);import "BDPI" function Bit#(32) checksum(Bit#(32) value);
import "BDPI" c_checksum =
function Bit#(32) checksum(Bit#(32) value);| Part | Meaning |
|---|---|
import "BDPI" | the function is implemented in C |
c_checksum = | the C symbol the simulator must find; without it, the BSV name is used |
function Bit#(32) checksum(...) | the type BSV callers see and the compiler checks |
The link name does not have to follow BSV capitalization rules, so a C name
such as c_checksum is fine.
1.2 Build steps
A BDPI design is built in four steps, and each step checks something different.
| Step | Command in the examples | Produces or checks |
|---|---|---|
| 1. BSV compile | bsc -verilog -u -g mkTop ... | mkTop.v, one .ba file per imported function, and VPI wrapper sources |
| 2. C compile | gcc -std=c11 -Wall -Wextra -Werror -c ... | the C object file |
| 3. Simulator link | bsc -verilog -vsim iverilog -e mkTop ... name.ba file.o | the simulator binary with the C code in a loadable library |
| 4. Simulate | vvp sim.out | loads the library, resolves the C symbols, runs |
Every imported function needs its own .ba file in step 3. A design that
imports logValue and nextCount passes both logValue.ba and
nextCount.ba.
Tip. Step 1 writes vpi_wrapper_<name>.c for each import. Near the top it
contains the C prototype that bsc expects, under the comment "the type of the
wrapped function". Compare your C prototype against that line.
1.3 Example: a differently named C function
ScalarBridge.bsv imports addBias, which is implemented by the C function
c_add_bias in scalar_bridge.c. Read the BDPI declaration and the C
prototype side by side.
At the call, 37 crosses as an 8-bit carrier and 5 as a 32-bit carrier. The
C function widens the first value, adds the two, and returns a 32-bit result.
Predict before you run.
- Which C type receives the
Bit#(8)argument, and which receivesBit#(32)? - Which symbol must the simulator find in the C object?
- What value is printed?
package ScalarBridge;
// Link the BSV name to a differently named C symbol.
import "BDPI" c_add_bias =
function Bit#(32) addBias(Bit#(8) value, Bit#(32) bias);
(* synthesize *)
module mkTop(Empty);
rule show;
$display("sum = %0d", addBias(8'd37, 32'd5));
$finish(0);
endrule
endmodule
endpackage#include <stdint.h>
// Bit#(32) crosses BDPI as a 32-bit unsigned carrier.
uint32_t c_add_bias(uint8_t value, uint32_t bias) {
return (uint32_t)value + bias;
}Practice this concept
Use this concept in a short BSV problem as your next step.
Check the result.
| Observation | What it proves |
|---|---|
sum = 42 | the C function ran and its result came back as the BSV value |
Foreign import file created: c_add_bias.ba | the .ba file is named after the link name, not the BSV name |
| a warning that Icarus Verilog's VPI support is incomplete | expected with this simulator; it does not affect the examples |
Try it yourself.
- Rename the C function to
c_add_bias2and leave the BSV declaration alone. BSC compiles the package, gcc compiles the C file, and the simulator binary is created, because nothing before simulation checks that the symbol exists. The failure appears whenvvploads the library: it reports an undefined symbolc_add_biasand exits with code 127. A misspelled link name is a load-time failure, not a BSV type error and not a C compile error.
2. C carrier types
2.1 The width selects the carrier
BDPI chooses the C type from the packed width of each argument and result.
| BSV argument or result | C carrier |
|---|---|
Bit#(0) through Bit#(8) | unsigned char |
Bit#(9) through Bit#(32) | unsigned int |
Bit#(33) through Bit#(64) | unsigned long long |
Bit#(65) and wider | unsigned int * |
polymorphic Bit#(n) | unsigned int * |
input String | char * |
import "BDPI" function Bit#(17) scale(Bit#(5) shift, Bit#(40) value);unsigned int scale(unsigned char shift, unsigned long long value);
| BSV type | Width class | Carrier |
|---|---|---|
Bit#(5) shift | 0 to 8 | unsigned char |
Bit#(40) value | 33 to 64 | unsigned long long |
Bit#(17) result | 9 to 32 | unsigned int |
A Bit#(17) still occupies a whole unsigned int; only its low 17 bits hold
BSV data. The examples use uint8_t and uint32_t, which have exactly the
widths these carriers need on the lab platform.
Note. The wrapper generated by this bsc version spells the 8-bit carrier
as plain char. It has the same size as unsigned char; using an unsigned
8-bit type on the C side keeps the bit pattern unambiguous.
Apply the table to a declaration you have not seen before.
import "BDPI" function Bit#(40) f(Bit#(9) a, Bit#(64) b, Bit#(65) c);Check
Which C prototype matches import BDPI function Bit#(40) f(Bit#(9) a, Bit#(64) b, Bit#(65) c);
2.2 String is input only
A String can be passed to C as char *, but it cannot be returned.
Declaring a String result fails with error T0087, which says the return
type of a foreign function cannot be String.
3. Packed types cross as bits
3.1 Bits decides the representation
Int, UInt, Bool, Maybe, structs, enums, and tagged unions can cross the
boundary when they have a Bits instance. BDPI packs them before the call and
unpacks the returned bits afterward.
import "BDPI" function Bool isZero(Int#(32) value);uint8_t isZero(uint32_t value); // Bool comes back in an 8-bit carrier
Packing keeps the BSV type contract, but it does not choose a signed C type.
Int#(32) and UInt#(32) both travel as a 32-bit unsigned carrier. Whether
those bits mean a signed or an unsigned number is decided by the C code.
| BSV value | Bits that reach C | Read as uint32_t |
|---|---|---|
Int#(32) -1 | all 32 bits set | 0xffffffff, the largest unsigned value |
Int#(32) -3 | two's complement of 3 | 0xfffffffd |
Bool True returned from C | 1 in the low bit | unpacked to True |
Structs, enums, and tagged unions follow the same rule: the C code must know
the exact layout produced by the BSV Bits instance.
Check
What C prototype does bsc expect for import BDPI function Bool inRange(Int#(16) v, UInt#(8) lo);
3.2 Example: a signed comparison in C
PackedSigned.bsv imports signedGreater, which takes two Int#(32) values.
packed_signed.c receives them as uint32_t and converts them back to signed
numbers in signed_bits before comparing. Read the BSV declaration and
signed_bits together. No sign conversion happens at the boundary.
Predict before you run.
- Which bit pattern does
-1arrive as? - What does
signedGreater(-1, 1)print? - What does
signedGreater(7, -3)print?
package PackedSigned;
// Int#(32) keeps signed intent while BDPI carries packed bits.
import "BDPI" c_signed_greater =
function Bool signedGreater(Int#(32) left, Int#(32) right);
(* synthesize *)
module mkTop(Empty);
rule show;
$display("negative greater = %0d", pack(signedGreater(-1, 1)));
$display("positive greater = %0d", pack(signedGreater(7, -3)));
$finish(0);
endrule
endmodule
endpackage#include <stdint.h>
static int32_t signed_bits(uint32_t bits) {
if ((bits & UINT32_C(0x80000000)) == 0) return (int32_t)bits;
return -1 - (int32_t)(~bits);
}
// Bool returns through an unsigned 8-bit carrier.
uint8_t c_signed_greater(uint32_t left, uint32_t right) {
return (uint8_t)(signed_bits(left) > signed_bits(right));
}Practice this concept
Use this concept in a short BSV problem as your next step.
Check the result.
| Output line | Value | What it proves |
|---|---|---|
negative greater | 0 | -1 > 1 is false, so C compared the values as signed numbers |
positive greater | 1 | 7 > -3 is true; -3 survived the boundary as its two's-complement pattern |
The BSV declaration accepts only Int#(32), so an unsigned value cannot be
passed by accident. The correct results come from signed_bits, which
restores the signed meaning on the C side.
Try it yourself.
- In C, compare the raw carriers with
left > rightand delete the unusedsigned_bitshelper. The build compiles C with-Werror, so an unused static function would stop it.-1arrives as0xffffffff, the largest unsigned value, so the two results flip to 1 and 0. The bits crossed the boundary unchanged; only the C interpretation changed. - Restore the C file and change the BSV parameter types to
UInt#(32). The build now stops in BSC, before C is involved, with errorT0051, "Literal -1 is not a valid UInt#(32)". Signed intent is enforced by the BSV declaration.
4. Wide values
4.1 Arrays of 32-bit words
A vector of 65 bits or more travels by pointer to an array of 32-bit words.
| BSV bit | Array position |
|---|---|
| bit 0 | bit 0 of word 0 |
| bit 31 | bit 31 of word 0 |
| bit 32 | bit 0 of word 1 |
| higher bits | later words, toward more significant bits |
BDPI passes the pointer but not the width. The C function must already know the width or receive it through another argument.
Check
A Bit#(96) argument arrives as unsigned int *a. Where does BSV bit 70 sit?
4.2 Wide results
A wide result reverses the direction. bsc allocates the result storage and
passes its pointer as the first C argument. The C function returns void and
fills the words; it does not allocate or free the buffer.
import "BDPI" function Bit#(96) mix96(Bit#(96) a);void mix96(unsigned int *result, unsigned int *a); // 3 words each
5. Polymorphic imports
5.1 Polymorphic widths always use a pointer
An imported Bit#(n) is polymorphic, so its C argument is unsigned int *
even when every actual call uses a small width.
import "BDPI" function Bit#(32) popcountPoly(Bit#(n) value);unsigned int popcountPoly(unsigned int *value); // width is not passed
Check
Every call of popcountPoly in the design passes a Bit#(8). What does the C parameter look like?
A simpler alternative is to keep the C boundary concrete and recover polymorphism in a BSV wrapper:
- import a C function that takes
Bit#(32), - extend the BSV value to 32 bits,
- pass
valueOf(n)as an explicit width argument, - truncate the returned value to
Bit#(n).
Syntax
function Bit#(n) wrapper(Bit#(32) value)
provisos (Add#(n, spare, 32));
return truncate(rawImport(value, fromInteger(valueOf(n))));
endfunction| Part | Role |
|---|---|
Add#(n, spare, 32) | proves that n fits in the 32-bit carrier |
valueOf(n) | turns the type-level width into a value |
fromInteger(...) | converts that value to the C argument's type |
truncate(...) | returns exactly n bits to the caller |
5.2 Example: a mask with an explicit width
WidthWrapper.bsv imports maskToWidthRaw, whose C side keeps only the low
width bits of a 32-bit value. The BSV wrapper maskToWidth supplies that
width from its own result type. Read maskToWidth and width_wrapper.c.
Predict before you run.
- What are the masks for widths 5 and 12?
- What does
Bit#(5) five = maskToWidth(32'h3f)print? - What does
Bit#(12) twelve = maskToWidth(32'habcd)print?
package WidthWrapper;
import "BDPI" c_mask_to_width =
function Bit#(32) maskToWidthRaw(Bit#(32) value, Bit#(8) width);
// Keep the C boundary concrete and recover polymorphism in BSV.
function Bit#(n) maskToWidth(Bit#(32) value)
provisos (Add#(n, spare, 32));
return truncate(maskToWidthRaw(value, fromInteger(valueOf(n))));
endfunction
(* synthesize *)
module mkTop(Empty);
rule show;
Bit#(5) five = maskToWidth(32'h3f);
Bit#(12) twelve = maskToWidth(32'habcd);
$display("five bits = %0h", five);
$display("twelve bits = %0h", twelve);
$finish(0);
endrule
endmodule
endpackage#include <stdint.h>
// The BSV wrapper passes the otherwise implicit vector width explicitly.
uint32_t c_mask_to_width(uint32_t value, uint8_t width) {
if (width == 0) return 0;
if (width >= 32) return value;
return value & ((UINT32_C(1) << width) - UINT32_C(1));
}Practice this concept
Use this concept in a short BSV problem as your next step.
Check the result.
| Output line | Value | What it proves |
|---|---|---|
five bits | 1f | valueOf(n) was 5, and C kept the low five bits of 0x3f |
twelve bits | bcd | valueOf(n) was 12, and C kept the low three hex digits of 0xabcd |
The C function never sees a polymorphic vector. The result type at each call chose the width, and the wrapper sent it explicitly.
Try it yourself.
- Change the type of
twelvetoBit#(16).valueOf(n)becomes 16, C keeps four digits, and the output isabcd. The C code did not change. - Change it to
Bit#(33).Add#(33, spare, 32)cannot hold, and BSC stops with errorT0031before any C code runs. The proviso keeps the concrete 32-bit boundary honest.
6. Return shapes
6.1 Value, Action, and ActionValue
An imported function may return a value, an Action, or an
ActionValue#(t). Pick the shape from what the C code actually does.
Syntax
import "BDPI" function ResultType pureName(...); // value
import "BDPI" function Action effectName(...); // effect only
import "BDPI" function ActionValue#(ResultType) bothName(...); // effect and resultimport "BDPI" function Action logValue(Bit#(32) v);
import "BDPI" function ActionValue#(Bit#(32)) nextCount();
rule step1;
logValue(32'd7);
endrule
rule step2;
let x <- nextCount();
$display("x=%0d", x);
endrulevoid logValue(uint32_t v); // Action: returns nothing uint32_t nextCount(void); // ActionValue: returns the result
| Shape | Use for | Scheduling meaning |
|---|---|---|
| value | repeatable computation with no side effects | may be evaluated whenever the expression is |
Action | changing C-side state or doing I/O | called exactly when the rule fires |
ActionValue | one ordered call that both has an effect and returns data | called when the rule fires; read the result with <- |
If the call count or order matters, declaring a pure value function makes a false promise to the compiler.
Check
Pick the import shape for three C helpers: (1) crc32 of its argument, no state; (2) append a line to a trace file; (3) pop the next stimulus word from a C-side queue.
6.2 Shared C resources
Several imported functions can share a C resource such as a file handle or a memory object. Pass handles between calls as packed bit vectors rather than exposing C pointers to BSV, and let the C side own the resource's lifetime.
7. Diagnose boundary failures
The failing step narrows the cause.
| Failure | Meaning |
|---|---|
| BSV type error | the declaration or wrapper is inconsistent before C takes part |
undefined symbol when vvp starts, exit code 127 | the link name and the C symbol differ, or the object was left out; the build itself succeeded |
| incorrect values or an abnormal termination during a C call | check the carrier widths and pointer shapes against the C prototype; a mismatch has no guaranteed result |
| right bits, wrong order or repeated effects | a stateful C call was declared as a pure value function |
Check
bsc, gcc and the simulator link all succeed. vvp then reports an undefined symbol c_mix and exits with code 127. What do you check first?
A reliable BDPI integration records five facts: the BSV function type, the exact C symbol, each packed width and its carrier, any explicit width or size argument, and whether the call has observable effects. Keep conversion policy in a small BSV wrapper and ABI details in a small C function.
8. Common mistakes
8.1 A link name that does not match the C symbol
// Wrong: C defines c_add_bias2, so vvp exits 127 with an undefined symbol
import "BDPI" c_add_bias =
function Bit#(32) addBias(Bit#(8) value, Bit#(32) bias);
// Right: the link name is exactly the C function name
import "BDPI" c_add_bias2 =
function Bit#(32) addBias(Bit#(8) value, Bit#(32) bias);No compile step looks for the symbol. Copy the name from the C source, and
remember that the .ba file passed to the link step is named after it too.
8.2 A scalar C parameter for a polymorphic import
import "BDPI" function Bit#(32) popcountPoly(Bit#(n) value);// Wrong: the argument is a pointer, not the number unsigned int popcountPoly(unsigned int value); // Right: polymorphic arguments arrive as an array of words unsigned int popcountPoly(unsigned int *value);
The polymorphic type forces the pointer ABI regardless of the actual width.
If you want a scalar carrier, import a concrete Bit#(32) function and wrap
it, as in the width-wrapper example.
8.3 Returning a String
// Wrong: T0087, the return type of a foreign function cannot be String
import "BDPI" function String greet(Bit#(8) id);
// Right: return packed data, and print it on the BSV side
import "BDPI" function Bit#(32) greetCode(Bit#(8) id);Only types that can be packed into a bit vector can come back from C.
8.4 Unsigned types for signed intent
// Wrong: T0051, "Literal -1 is not a valid UInt#(32)"
import "BDPI" c_signed_greater =
function Bool signedGreater(UInt#(32) left, UInt#(32) right);
// Right: the BSV type states the signed meaning
import "BDPI" c_signed_greater =
function Bool signedGreater(Int#(32) left, Int#(32) right);Both versions use the same C carrier, so the C code cannot tell them apart. Only the BSV declaration can stop an unsigned value from being passed where a signed one is meant.
8.5 A stateful C function declared as a value
// Wrong: a counter that changes on every call, declared as pure
import "BDPI" function Bit#(32) nextCount();
// Right: the effect and the result belong to one ordered call
import "BDPI" function ActionValue#(Bit#(32)) nextCount();A value function promises that calling it again gives the same result. A counter breaks that promise, and the compiler is free to evaluate the call as often as the expression appears.
9. Summary
| Topic | Key point |
|---|---|
| Declaration | import "BDPI" [c_symbol =] function ...; the link name is the C symbol and names the .ba file |
| Build | BSV compile, C compile, link with every .ba and the object, then vvp; a missing symbol fails only at load time |
| Carriers | width 0-8: unsigned char; 9-32: unsigned int; 33-64: unsigned long long; wider or polymorphic: unsigned int * |
| Packed types | Bits types cross as raw bits; signed meaning is kept by the BSV type and restored in C |
| Wide values | arrays of 32-bit words, bit 0 in word 0; wide results arrive as a first pointer argument |
| Polymorphism | polymorphic Bit#(n) uses pointers; a concrete import plus a BSV wrapper keeps a scalar carrier |
| Return shape | value for pure computation, Action for effects, ActionValue for both |
10. Check your understanding
-
Why do
Int#(32)andUInt#(32)use the same C carrier?Show answer
BDPI picks the carrier from the packed width only. Both types pack to 32 bits, so both travel as a 32-bit unsigned value. The signed meaning lives in the BSV type and must be restored by the C code.
-
Why does a polymorphic
Bit#(n)use a pointer even when a proviso boundsn?Show answer
The C function is compiled once and must serve every width the BSV type allows. Only the pointer ABI works for an unknown width, and a proviso does not change the imported function's type.
-
A link name is misspelled. Which step fails, and why does the build still succeed?
Show answer
Simulation fails:
vvpreports an undefined symbol when it loads the C library and exits with code 127. Neitherbsc, gcc, nor the link step checks that the named symbol exists. -
What is the C prototype for
import "BDPI" function Bit#(96) mix96(Bit#(96) a);?Show answer
void mix96(unsigned int *result, unsigned int *a);. The 96-bit result is written through a pointer passed as the first argument, and the 96-bit argument arrives as a pointer to three 32-bit words. -
A C helper writes a log entry and returns a status code. Which return shape should its import use?
Show answer
ActionValue#(t). The call has an effect that must happen exactly when the rule fires, and it also returns data. A value function would falsely promise that the call has no effect.