Skip to content
code0.HDL

Embedding C in a BSV design

Call a C function from BSV with import BDPI: the declaration, the carrier types each side sees, packed and wide values, polymorphic imports, return shapes, and how boundary failures look.

In this chapter

  • Declare an imported C function and match its BSV type to the C prototype
  • Predict how a packed BSV value appears on the C side, including wide values
  • Diagnose a boundary failure from the link or run-time symptom it produces
  • Choose a value, Action, or ActionValue import from what the C code does
Concepts in this chapter
  • Embedding C in a BSV design
Prerequisite concepts

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);
PartMeaning
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.

StepCommand in the examplesProduces or checks
1. BSV compilebsc -verilog -u -g mkTop ...mkTop.v, one .ba file per imported function, and VPI wrapper sources
2. C compilegcc -std=c11 -Wall -Wextra -Werror -c ...the C object file
3. Simulator linkbsc -verilog -vsim iverilog -e mkTop ... name.ba file.othe simulator binary with the C code in a loadable library
4. Simulatevvp sim.outloads 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.

  1. Which C type receives the Bit#(8) argument, and which receives Bit#(32)?
  2. Which symbol must the simulator find in the C object?
  3. What value is printed?
scalar_bridge.c
#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.

ObservationWhat it proves
sum = 42the C function ran and its result came back as the BSV value
Foreign import file created: c_add_bias.bathe .ba file is named after the link name, not the BSV name
a warning that Icarus Verilog's VPI support is incompleteexpected with this simulator; it does not affect the examples

Try it yourself.

  1. Rename the C function to c_add_bias2 and 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 when vvp loads the library: it reports an undefined symbol c_add_bias and 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 resultC 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 widerunsigned int *
polymorphic Bit#(n)unsigned int *
input Stringchar *
import "BDPI" function Bit#(17) scale(Bit#(5) shift, Bit#(40) value);
unsigned int scale(unsigned char shift, unsigned long long value);
BSV typeWidth classCarrier
Bit#(5) shift0 to 8unsigned char
Bit#(40) value33 to 64unsigned long long
Bit#(17) result9 to 32unsigned 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);

Your answer

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 valueBits that reach CRead as uint32_t
Int#(32) -1all 32 bits set0xffffffff, the largest unsigned value
Int#(32) -3two's complement of 30xfffffffd
Bool True returned from C1 in the low bitunpacked 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);

Your answer

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.

  1. Which bit pattern does -1 arrive as?
  2. What does signedGreater(-1, 1) print?
  3. What does signedGreater(7, -3) print?
PackedSigned.bsv
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
packed_signed.c
#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 lineValueWhat it proves
negative greater0-1 > 1 is false, so C compared the values as signed numbers
positive greater17 > -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.

  1. In C, compare the raw carriers with left > right and delete the unused signed_bits helper. The build compiles C with -Werror, so an unused static function would stop it. -1 arrives as 0xffffffff, the largest unsigned value, so the two results flip to 1 and 0. The bits crossed the boundary unchanged; only the C interpretation changed.
  2. Restore the C file and change the BSV parameter types to UInt#(32). The build now stops in BSC, before C is involved, with error T0051, "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 bitArray position
bit 0bit 0 of word 0
bit 31bit 31 of word 0
bit 32bit 0 of word 1
higher bitslater 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?

Your answer

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?

Your answer

A simpler alternative is to keep the C boundary concrete and recover polymorphism in a BSV wrapper:

  1. import a C function that takes Bit#(32),
  2. extend the BSV value to 32 bits,
  3. pass valueOf(n) as an explicit width argument,
  4. 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
PartRole
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.

  1. What are the masks for widths 5 and 12?
  2. What does Bit#(5) five = maskToWidth(32'h3f) print?
  3. What does Bit#(12) twelve = maskToWidth(32'habcd) print?
WidthWrapper.bsv
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
width_wrapper.c
#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 lineValueWhat it proves
five bits1fvalueOf(n) was 5, and C kept the low five bits of 0x3f
twelve bitsbcdvalueOf(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.

  1. Change the type of twelve to Bit#(16). valueOf(n) becomes 16, C keeps four digits, and the output is abcd. The C code did not change.
  2. Change it to Bit#(33). Add#(33, spare, 32) cannot hold, and BSC stops with error T0031 before 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 result
import "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);
endrule
void logValue(uint32_t v);      // Action: returns nothing
uint32_t nextCount(void);       // ActionValue: returns the result
ShapeUse forScheduling meaning
valuerepeatable computation with no side effectsmay be evaluated whenever the expression is
Actionchanging C-side state or doing I/Ocalled exactly when the rule fires
ActionValueone ordered call that both has an effect and returns datacalled 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.

Your answer

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.

FailureMeaning
BSV type errorthe declaration or wrapper is inconsistent before C takes part
undefined symbol when vvp starts, exit code 127the 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 callcheck the carrier widths and pointer shapes against the C prototype; a mismatch has no guaranteed result
right bits, wrong order or repeated effectsa 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?

Your answer

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

// 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

TopicKey point
Declarationimport "BDPI" [c_symbol =] function ...; the link name is the C symbol and names the .ba file
BuildBSV compile, C compile, link with every .ba and the object, then vvp; a missing symbol fails only at load time
Carrierswidth 0-8: unsigned char; 9-32: unsigned int; 33-64: unsigned long long; wider or polymorphic: unsigned int *
Packed typesBits types cross as raw bits; signed meaning is kept by the BSV type and restored in C
Wide valuesarrays of 32-bit words, bit 0 in word 0; wide results arrive as a first pointer argument
Polymorphismpolymorphic Bit#(n) uses pointers; a concrete import plus a BSV wrapper keeps a scalar carrier
Return shapevalue for pure computation, Action for effects, ActionValue for both

10. Check your understanding

  1. Why do Int#(32) and UInt#(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.

  2. Why does a polymorphic Bit#(n) use a pointer even when a proviso bounds n?

    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.

  3. A link name is misspelled. Which step fails, and why does the build still succeed?

    Show answer

    Simulation fails: vvp reports an undefined symbol when it loads the C library and exits with code 127. Neither bsc, gcc, nor the link step checks that the named symbol exists.

  4. 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.

  5. 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.

Checkpoint

Run the example and compare it with your prediction

Sources

Licences and notices for the sources this site follows