From cb0413e384bdc6ce200e072053258216995fccaf Mon Sep 17 00:00:00 2001 From: Fabian Schuiki Date: Wed, 28 Jan 2026 15:51:46 -0800 Subject: [PATCH] Allow ChiselSim to link in prebuilt software libraries Add the `libraries` and `libraryPaths` settings to ChiselSim's `Settings` class that can be passed to `simulate(...)`. These settings allow the user to provide a list of library names or paths that the simulator backend should link into the simulator. I envision this to be used for DPI-based tests to link in implementations for DPI functions. The build system can use the `CHISELSIM_LIBS` environment variable or the `chiselsim.libraries` Java property to provide a list of library names and their paths which the user can call out in `libraries`. This allows the Chisel code to not hardcode filesystem paths that are likely to be a build system concern, and instead only call out libraries by name. This commit also adds a small C compiler invocation to `build.mill` in order to build a handful of shared libraries that the simulator unit tests will use to check the library linking behavior. --- build.mill | 33 ++++++++++ .../scala/chisel3/simulator/Settings.scala | 66 +++++++++++++++++-- .../scala/chisel3/simulator/Simulator.scala | 36 ++++++++++ .../resources/chisel3/simulator/linkLibA.c | 1 + .../resources/chisel3/simulator/linkLibB.c | 1 + .../resources/chisel3/simulator/linkLibC.c | 1 + .../chiselTests/simulator/SimulatorSpec.scala | 32 +++++++++ svsim/package.mill | 4 ++ svsim/src/main/scala/Backend.scala | 5 +- svsim/src/main/scala/vcs/Backend.scala | 2 + svsim/src/main/scala/verilator/Backend.scala | 2 + 11 files changed, 177 insertions(+), 6 deletions(-) create mode 100644 src/test/resources/chisel3/simulator/linkLibA.c create mode 100644 src/test/resources/chisel3/simulator/linkLibB.c create mode 100644 src/test/resources/chisel3/simulator/linkLibC.c diff --git a/build.mill b/build.mill index 6406325926f..69b2f3900e4 100644 --- a/build.mill +++ b/build.mill @@ -338,6 +338,39 @@ trait Chisel extends CrossSbtModule with HasScala2MacroAnno with HasScalaPlugin override def scalacOptions = Task { super.scalacOptions() :+ "-Wconf:cat=other-implicit-type:s" } override def testForkGrouping = discoveredTestClasses().grouped(8).toSeq + + // Compile shared libraries for the simulator link library tests. + def sharedTestLibs: T[Seq[os.Path]] = Task(persistent = true) { + val srcDir = this.moduleDir / "src" / "test" / "resources" / "chisel3" / "simulator" + val cc = sys.env.getOrElse("CC", "cc") + val shared = if (scala.util.Properties.isMac) "-dynamiclib" else "-shared" + val srcFiles = Seq("linkLibA.c", "linkLibB.c", "linkLibC.c") + val libFiles = srcFiles.map { src => + val lib = Task.dest / (src.stripSuffix(".c") + ".so") + os.proc(cc, shared, "-fPIC", srcDir / src, "-o", lib).call() + lib + } + libFiles + } + + // Pass some of the pre-compiled shared libraries to the simulator via + // environment variables. + override def forkEnv = Task { + val libs = sharedTestLibs() + super.forkEnv() ++ Map( + // CHISELSIM_LIBS lets ChiselSim resolve libraries by name: + "CHISELSIM_LIBS" -> s"linkLibA=${libs(0)}", + // Also pass in a full path to test the `libraryPaths` setting: + "LINKLIBC_FULL_PATH" -> libs(2).toString + ) + } + + // Pass one of the pre-compiled shared libraries to the simulator via a JVM + // property. + override def forkArgs = Task { + val libs = sharedTestLibs() + super.forkArgs() :+ s"-Dchiselsim.libraries=linkLibB=${libs(1)}" + } } def unitTest( diff --git a/src/main/scala/chisel3/simulator/Settings.scala b/src/main/scala/chisel3/simulator/Settings.scala index 885b9b1fb4e..bd11be88d01 100644 --- a/src/main/scala/chisel3/simulator/Settings.scala +++ b/src/main/scala/chisel3/simulator/Settings.scala @@ -81,6 +81,12 @@ object MacroText { * @param enableWavesAtTimeZero enable waveform dumping at time zero. This * requires a simulator capable of dumping waves. * @param randomization random initialization settings to use + * @param libraries Names of libraries to include in simulation. Use this to + * provide implementations for DPI functions, for example. The simulator will + * resolve these libraries to concrete files using the `CHISELSIM_LIBS` + * environment variable and `chiselsim.libraries` Java property. + * @param libraryPaths Paths to libraries to include in simulation. Use this to + * provide implementations for DPI functions, for example. */ final class Settings[A <: RawModule] private[simulator] ( /** Layers to turn on/off during Verilog elaboration */ @@ -90,7 +96,9 @@ final class Settings[A <: RawModule] private[simulator] ( val stopCond: Option[MacroText.Type[A]], val plusArgs: Seq[svsim.PlusArg], val enableWavesAtTimeZero: Boolean, - val randomization: Randomization + val randomization: Randomization, + val libraries: Seq[String], + val libraryPaths: Seq[String] ) { def copy( @@ -102,7 +110,43 @@ final class Settings[A <: RawModule] private[simulator] ( enableWavesAtTimeZero: Boolean = enableWavesAtTimeZero, randomization: Randomization = randomization ) = - new Settings(verilogLayers, assertVerboseCond, printfCond, stopCond, plusArgs, enableWavesAtTimeZero, randomization) + new Settings( + verilogLayers, + assertVerboseCond, + printfCond, + stopCond, + plusArgs, + enableWavesAtTimeZero, + randomization, + libraries, + libraryPaths + ) + + def withLibraries(libraries: Seq[String]): Settings[A] = + new Settings( + verilogLayers, + assertVerboseCond, + printfCond, + stopCond, + plusArgs, + enableWavesAtTimeZero, + randomization, + libraries, + libraryPaths + ) + + def withLibraryPaths(libraryPaths: Seq[String]): Settings[A] = + new Settings( + verilogLayers, + assertVerboseCond, + printfCond, + stopCond, + plusArgs, + enableWavesAtTimeZero, + randomization, + libraries, + libraryPaths + ) private[simulator] def preprocessorDefines( elaboratedModule: ElaboratedModule[A] @@ -150,7 +194,9 @@ object Settings { stopCond = Some(MacroText.NotSignal(get = _.reset)), plusArgs = Seq.empty, enableWavesAtTimeZero = false, - randomization = Randomization.random + randomization = Randomization.random, + libraries = Seq.empty, + libraryPaths = Seq.empty ) /** Retun a default [[Settings]] for a [[RawModule]]. @@ -179,7 +225,9 @@ object Settings { stopCond = None, plusArgs = Seq.empty, enableWavesAtTimeZero = false, - randomization = Randomization.random + randomization = Randomization.random, + libraries = Seq.empty, + libraryPaths = Seq.empty ) /** Simple factory for construcing a [[Settings]] from arguments. @@ -193,6 +241,12 @@ object Settings { * @param printfCond a condition that guards printing of [[chisel3.printf]]s * @param stopCond a condition that guards terminating the simulation (via * `$fatal`) for asserts created from `circt_chisel_ifelsefatal` intrinsics + * @param libraries Names of libraries to include in simulation. Use this to + * provide implementations for DPI functions, for example. The simulator will + * resolve these libraries to concrete files using the `CHISELSIM_LIBS` + * environment variable and `chiselsim.libraries` Java property. + * @param libraryPaths Paths to libraries to include in simulation. Use this + * to provide implementations for DPI functions, for example. * @return a [[Settings]] with the provided parameters set */ def apply[A <: RawModule]( @@ -210,7 +264,9 @@ object Settings { stopCond = stopCond, plusArgs = plusArgs, enableWavesAtTimeZero = enableWavesAtTimeZero, - randomization = randomization + randomization = randomization, + libraries = Seq.empty, + libraryPaths = Seq.empty ) } diff --git a/src/main/scala/chisel3/simulator/Simulator.scala b/src/main/scala/chisel3/simulator/Simulator.scala index ec385ca333d..8bcf55a78ef 100644 --- a/src/main/scala/chisel3/simulator/Simulator.scala +++ b/src/main/scala/chisel3/simulator/Simulator.scala @@ -262,6 +262,41 @@ trait Simulator[T <: Backend] { } Files.walkFileTree(Paths.get(workspace.primarySourcesPath), new DirectoryFinder) + // Collect a list of library names and paths from the environment. These + // will be used to resolve library names provided by the user. Environment + // variables take precedence over Java properties. + def parseLibraryMap(value: Option[String]): Map[String, String] = { + value + .getOrElse("") + .split(":") + .filter(_.nonEmpty) + .map { entry => + entry.split("=", 2) match { + case Array(name, path) => name -> path + case _ => + throw new IllegalArgumentException( + s"Invalid link library mapping `$entry`; expected `=`" + ) + } + } + .toMap + } + val libraryMapFromProp = parseLibraryMap(sys.props.get("chiselsim.libraries")) + val libraryMapFromEnv = parseLibraryMap(sys.env.get("CHISELSIM_LIBS")) + val libraryMap = libraryMapFromProp ++ libraryMapFromEnv + val resolvedLibraryPaths = settings.libraries.map { name => + libraryMap.getOrElse( + name, + throw new NoSuchElementException( + s"Link library `$name` not found. " + + "Set the `CHISELSIM_LIBS` environment variable or the " + + "`chiselsim.libraries` Java property to provide a list of " + + "`=` mappings separated by `:`. Available libraries: " + + s"[${libraryMap.keys.mkString(", ")}]" + ) + ) + } + val commonCompilationSettingsUpdated = commonSettingsModifications( commonCompilationSettings.copy( // Append to the include directorires based on what the @@ -275,6 +310,7 @@ trait Simulator[T <: Backend] { directoryFilter = commonCompilationSettings.directoryFilter.orElse( settings.verilogLayers.shouldIncludeDirectory(elaboratedModule, workspace.primarySourcesPath) ), + linkLibraryPaths = commonCompilationSettings.linkLibraryPaths ++ resolvedLibraryPaths ++ settings.libraryPaths, simulationSettings = commonCompilationSettings.simulationSettings.copy( plusArgs = commonCompilationSettings.simulationSettings.plusArgs ++ settings.plusArgs, enableWavesAtTimeZero = diff --git a/src/test/resources/chisel3/simulator/linkLibA.c b/src/test/resources/chisel3/simulator/linkLibA.c new file mode 100644 index 00000000000..03606c73039 --- /dev/null +++ b/src/test/resources/chisel3/simulator/linkLibA.c @@ -0,0 +1 @@ +void magicFuncA(int *result) { *result = 42; } diff --git a/src/test/resources/chisel3/simulator/linkLibB.c b/src/test/resources/chisel3/simulator/linkLibB.c new file mode 100644 index 00000000000..3e65a628d6a --- /dev/null +++ b/src/test/resources/chisel3/simulator/linkLibB.c @@ -0,0 +1 @@ +void magicFuncB(int *result) { *result = 1337; } diff --git a/src/test/resources/chisel3/simulator/linkLibC.c b/src/test/resources/chisel3/simulator/linkLibC.c new file mode 100644 index 00000000000..b54978a88dc --- /dev/null +++ b/src/test/resources/chisel3/simulator/linkLibC.c @@ -0,0 +1 @@ +void magicFuncC(int *result) { *result = 9001; } diff --git a/src/test/scala-2/chiselTests/simulator/SimulatorSpec.scala b/src/test/scala-2/chiselTests/simulator/SimulatorSpec.scala index a757f8bf979..97c058b7bd6 100644 --- a/src/test/scala-2/chiselTests/simulator/SimulatorSpec.scala +++ b/src/test/scala-2/chiselTests/simulator/SimulatorSpec.scala @@ -3,6 +3,7 @@ package chiselTests.simulator import chisel3._ import chisel3.layer.{block, Convention, Layer, LayerConfig} import chisel3.simulator._ +import chisel3.util.circt.dpi._ import org.scalatest.funspec.AnyFunSpec import org.scalatest.matchers.must.Matchers import org.scalatest.matchers.should.Matchers.convertToAnyShouldWrapper @@ -15,6 +16,21 @@ class VerilatorSimulator(val workspacePath: String) extends Simulator[verilator. val backendSpecificCompilationSettings = verilator.Backend.CompilationSettings.default } +object SimulatorSpec { + class LinkLibraryTest extends RawModule { + val a = IO(Output(UInt(32.W))) + val b = IO(Output(UInt(32.W))) + val c = IO(Output(UInt(32.W))) + // The following DPI functions are implemented in + // `src/test/resources/chisel3/simulator/linkLib*.c`. The build system + // compiles these into shared libraries and makes them available to be linked + // into the simulator above. + a := RawUnclockedNonVoidFunctionCall("magicFuncA", UInt(32.W))(true.B) + b := RawUnclockedNonVoidFunctionCall("magicFuncB", UInt(32.W))(true.B) + c := RawUnclockedNonVoidFunctionCall("magicFuncC", UInt(32.W))(true.B) + } +} + class SimulatorSpec extends AnyFunSpec with Matchers { describe("Chisel Simulator") { it("runs GCD correctly") { @@ -333,5 +349,21 @@ class SimulatorSpec extends AnyFunSpec with Matchers { .result } + + it("supports link libraries provided by the build system") { + val simulator = new VerilatorSimulator("test_run_dir/simulator/LinkLibrary") + val settings = Settings + .defaultRaw[SimulatorSpec.LinkLibraryTest] + .withLibraries(Seq("linkLibA", "linkLibB")) + .withLibraryPaths(Seq(sys.env("LINKLIBC_FULL_PATH"))) + val result = simulator + .simulate(new SimulatorSpec.LinkLibraryTest(), settings = settings) { module => + import PeekPokeAPI._ + val dut = module.wrapped + (dut.a.peek().litValue, dut.b.peek().litValue, dut.c.peek().litValue) + } + .result + assert(result === (42, 1337, 9001)) + } } } diff --git a/svsim/package.mill b/svsim/package.mill index 5051e104e32..4576efad084 100644 --- a/svsim/package.mill +++ b/svsim/package.mill @@ -23,6 +23,10 @@ trait Svsim extends ChiselCrossModule with HasCommonOptions with ScalafmtModule ) } + def compileMvnDeps = Seq(mvn"com.lihaoyi::unroll-annotation:0.3.0") + + def scalacPluginMvnDeps = Seq(mvn"com.lihaoyi:::unroll-plugin:0.3.0") + object test extends SbtTests with TestModule.ScalaTest with ScalafmtModule { def mvnDeps = Seq(v.scalatest, v.scalacheck) } diff --git a/svsim/src/main/scala/Backend.scala b/svsim/src/main/scala/Backend.scala index 1b46f42c1c9..426e606a3d2 100644 --- a/svsim/src/main/scala/Backend.scala +++ b/svsim/src/main/scala/Backend.scala @@ -1,5 +1,6 @@ package svsim +import com.lihaoyi.unroll import java.io.File import scala.util.matching.Regex @@ -68,7 +69,9 @@ case class CommonCompilationSettings( includeDirs: Option[Seq[String]] = None, fileFilter: PartialFunction[File, Boolean] = PartialFunction.empty, directoryFilter: PartialFunction[File, Boolean] = PartialFunction.empty, - simulationSettings: CommonSimulationSettings = CommonSimulationSettings.default + simulationSettings: CommonSimulationSettings = CommonSimulationSettings.default, + // @unroll is required to maintain binary compatibility + @unroll linkLibraryPaths: Seq[String] = Seq.empty ) object CommonCompilationSettings { object VerilogPreprocessorDefine { diff --git a/svsim/src/main/scala/vcs/Backend.scala b/svsim/src/main/scala/vcs/Backend.scala index 5f4b82766d4..a88b8637634 100644 --- a/svsim/src/main/scala/vcs/Backend.scala +++ b/svsim/src/main/scala/vcs/Backend.scala @@ -351,6 +351,8 @@ final class Backend( case Some(paths) => paths.flatMap(Seq("-y", _)) }, + commonSettings.linkLibraryPaths.flatMap(lib => Seq("-sv_lib", lib)), + commonSettings.includeDirs match { case None => Seq() case Some(dirs) => dirs.map(dir => s"+incdir+$dir") diff --git a/svsim/src/main/scala/verilator/Backend.scala b/svsim/src/main/scala/verilator/Backend.scala index 1332b088862..cb5509e0825 100644 --- a/svsim/src/main/scala/verilator/Backend.scala +++ b/svsim/src/main/scala/verilator/Backend.scala @@ -333,6 +333,8 @@ final class Backend(executablePath: String) extends svsim.Backend { paths.foreach(p => addArg(Seq("-y", p))) } + commonSettings.linkLibraryPaths.foreach(lib => addArg(Seq(lib))) + commonSettings.includeDirs.foreach { dirs => addArg(dirs.map(dir => s"+incdir+$dir")) }