<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sivansh Gupta</title>
    <description>The latest articles on DEV Community by Sivansh Gupta (@sivansh).</description>
    <link>https://dev.to/sivansh</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3354083%2F4180cf34-a450-4d49-8412-e00b63d5ff39.jpeg</url>
      <title>DEV Community: Sivansh Gupta</title>
      <link>https://dev.to/sivansh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sivansh"/>
    <language>en</language>
    <item>
      <title>Why RISC-V Might Be Perfect for Your Next Scripting Engine Backend</title>
      <dc:creator>Sivansh Gupta</dc:creator>
      <pubDate>Mon, 14 Jul 2025 15:30:18 +0000</pubDate>
      <link>https://dev.to/sivansh/why-risc-v-is-perfect-for-your-next-scripting-engine-backend-5goi</link>
      <guid>https://dev.to/sivansh/why-risc-v-is-perfect-for-your-next-scripting-engine-backend-5goi</guid>
      <description>&lt;h2&gt;
  
  
  A RISC-V Backend for a Scripting Engine
&lt;/h2&gt;

&lt;p&gt;Seeing this title, you may have a lot of questions, like "What? Why? Why not WASM?" All of these are valid questions, and in this article, I will be answering them and a lot more!&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Principles
&lt;/h2&gt;

&lt;p&gt;The end goal of this project is to have a portable, performant, safe, and non-opinionated scripting backend, primarily for use in a game engine.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Portable:&lt;/strong&gt; Scripts should be compiled once and run anywhere the engine runs. We should be able to share the compiled script binaries without worrying about the host's architecture or operating system.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Performant:&lt;/strong&gt; The scripts must execute at the highest possible performance with little to no overhead.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Safe:&lt;/strong&gt; As the system is intended to support modding, it must be a secure sandbox. Scripts should not be able to affect the host system outside of the controlled environment I provide.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Non-Opinionated:&lt;/strong&gt; The system should not restrict developers to a single language. Any language that can target the chosen architecture, like C, C++, or Rust, should be usable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Not WASM?
&lt;/h2&gt;

&lt;p&gt;To be honest, WASM fits most of these criteria. It was designed to be portable and safe, has performant runtimes, and can be targeted by most system languages.&lt;/p&gt;

&lt;p&gt;However, WASM is a stack-based virtual machine. Real hardware is almost universally register-based. This mismatch means that it's challenging for hardware to efficiently pipeline and execute stack-based instructions. While this is often solved with JIT (Just-In-Time) compilation, it adds complexity and can make the unoptimized, interpreted performance slower. I want my interpreter to be as fast as possible from the start.&lt;/p&gt;

&lt;p&gt;For a great explanation of stack vs. register machines, check out &lt;a href="https://www.youtube.com/watch?v=cMMAGIefZuM" rel="noopener noreferrer"&gt;this video&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing RISC-V
&lt;/h2&gt;

&lt;p&gt;This is where RISC-V comes in. It's an open-standard instruction set architecture (ISA) that perfectly aligns with our core principles.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;It's a Register Machine:&lt;/strong&gt; As a modern RISC architecture, it's designed to map efficiently to hardware, which allows for a more performant interpreter.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;It's an Open Standard:&lt;/strong&gt; The base instruction set is frozen and has a wide variety of standard extensions (like for multiplication/division, atomic operations, etc.). This means we can create a minimal, custom "profile" for our scripts to target, ensuring portability.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;It's Simple:&lt;/strong&gt; The base integer ISA is so simple you can fit it on a single sheet of paper. This drastically reduces the complexity of building an emulator.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Execution Environment
&lt;/h2&gt;

&lt;p&gt;To run RISC-V binaries, we need an emulator that acts as a virtual machine. This emulator is our sandbox, giving us complete control over how the script executes and interacts with the outside world.&lt;/p&gt;

&lt;p&gt;A diagram of the architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+--------------------------------+
|       Host Application         |
|        (Game Engine)           |
+--------------------------------+
             ^
             | C++ Function Calls
             v
+--------------------------------+
|       Dawn RISC-V Emulator     |
|  (Manages Memory &amp;amp; `ecall`s)   |
+--------------------------------+
             ^
             | `ecall` instruction
             v
+--------------------------------+
|        Guest Script            |
|      (RISC-V Binary)           |
+--------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Privilege and Machine Mode
&lt;/h3&gt;

&lt;p&gt;The RISC-V specification defines several privilege levels (User, Supervisor, and Machine). A full-fledged operating system would use these to isolate processes from each other and from the kernel. However, for a scripting environment, this is unnecessary complexity.&lt;/p&gt;

&lt;p&gt;My emulator implements only &lt;strong&gt;Machine Mode&lt;/strong&gt;, the highest privilege level. While this sounds like it gives the script a lot of power, it's confined entirely within the emulator. The script thinks it has full control of the "hardware," but that hardware is completely virtual and managed by the host application. This simplifies the design immensely—there's no need to emulate a complex Memory Management Unit (MMU) or handle context switches.&lt;/p&gt;

&lt;h3&gt;
  
  
  The ELF Format and Memory
&lt;/h3&gt;

&lt;p&gt;ELF (Executable and Linkable Format) is a widely used standard for storing compiled programs. An ELF file contains the machine code, data, and metadata crucial for the program's execution.&lt;/p&gt;

&lt;p&gt;A key aspect of ELF is its organization into loadable sections. These sections represent different parts of the program, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;.text: Contains the executable machine code instructions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;.data: Holds initialized global and static variables.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;.rodata: Stores read-only data, like string literals.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;.bss: Reserves space for uninitialized global and static variables (these don't take up space in the file but are allocated at runtime and zeroed out).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The ELF file also includes an ELF header and a program header table. The ELF header provides a "roadmap" to the file's structure, including information about the entry point, which indicates where the program execution should begin. The program header table, in particular, describes how the various loadable sections (often grouped into segments) should be placed into memory when the program is run. Each entry in this table specifies the size, file offset, and memory location for a particular segment.&lt;/p&gt;

&lt;p&gt;While ELF offers robust capabilities for complex memory management and shared libraries, simpler execution environments might benefit from alternative formats. One such alternative is BFLT (Binary Flat Format). BFLT is a lightweight format that streamlines the loading process by typically consolidating all code and data into a single, contiguous block. This eliminates the need for intricate memory mapping mechanisms, making it trivial to load and potentially faster for specific use cases, especially in environments without a Memory Management Unit (MMU).&lt;/p&gt;

&lt;h3&gt;
  
  
  Memory Management and Sharing
&lt;/h3&gt;

&lt;p&gt;The guest's memory is not one single, monolithic block. Instead, the emulator manages a collection of memory ranges. This design allows for powerful and efficient ways for the host application to interact with the guest script.&lt;/p&gt;

&lt;p&gt;The core of this system is the &lt;code&gt;memory_t::insert_memory(uintptr_t address, size_t size)&lt;/code&gt; function. The host can allocate a piece of its own memory (e.g., using &lt;code&gt;new&lt;/code&gt; or &lt;code&gt;malloc&lt;/code&gt;) and then call &lt;code&gt;insert_memory&lt;/code&gt; to make that specific block of host memory accessible to the guest. The emulator maintains a list of these valid memory ranges.&lt;/p&gt;

&lt;p&gt;When the guest program tries to access a memory address, the emulator first checks if the address falls within any of the registered ranges. This includes the main emulated RAM (which is itself just another range inserted at startup) and any shared memory blocks the host has inserted.&lt;/p&gt;

&lt;p&gt;This approach provides a highly efficient mechanism for sharing data. Instead of copying data back and forth, the host and guest can operate on the exact same memory. The process looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Host Allocates Memory:&lt;/strong&gt; The host application allocates a block of memory. This memory might contain game state, a frame buffer, or any other data structure.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Host Inserts Memory:&lt;/strong&gt; The host calls &lt;code&gt;machine._memory.insert_memory(...)&lt;/code&gt; with the pointer and size of the allocated block. The emulator adds this host memory region to the guest's list of accessible memory ranges.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Host Provides Address to Guest:&lt;/strong&gt; The raw host pointer is meaningless to the guest. The host must use a function like &lt;code&gt;machine._memory.translate_host_to_guest_virtual(...)&lt;/code&gt; to get a guest-visible virtual address for the shared block. This virtual address is then passed to the guest, typically via a custom syscall.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Guest Accesses Memory:&lt;/strong&gt; The guest can now use standard pointer operations to read from and write to the shared memory block using the virtual address it received. The emulator transparently handles the address translation, ensuring the guest's operations are safely applied to the correct host memory region.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  System Calls: The Bridge to the Host
&lt;/h3&gt;

&lt;p&gt;A sandboxed script is useless if it can't interact with the host. It needs to be able to print messages, request memory, or interact with game world objects. In RISC-V, the &lt;code&gt;ecall&lt;/code&gt; (environment call) instruction is used for this purpose.&lt;/p&gt;

&lt;p&gt;When a script executes an &lt;code&gt;ecall&lt;/code&gt;, the emulator traps it and pauses execution. It then inspects the script's registers to determine what it wants to do (e.g., register &lt;code&gt;a7&lt;/code&gt; might hold the syscall number, and &lt;code&gt;a0-a2&lt;/code&gt; might hold arguments).&lt;/p&gt;

&lt;p&gt;This creates a secure and well-defined bridge between the script and the engine. Here’s a practical example:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Host (C++) side, setting up shared memory and a custom syscall:&lt;/strong&gt;&lt;br&gt;
This example demonstrates how the host can allocate a block of memory and make it accessible to the guest program. This is a powerful feature for allowing scripts to interact with host data structures.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Host-side example from examples/simple/main.cpp&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;cstring&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;iomanip&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;iostream&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"machine.hpp"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="c1"&gt;// Syscall handlers for exit&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;exit_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dawn&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;machine_t&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;machine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;machine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_running&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;argc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;argc&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Initialize the machine&lt;/span&gt;
  &lt;span class="n"&gt;dawn&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;machine_t&lt;/span&gt; &lt;span class="n"&gt;machine&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// Register standard syscalls&lt;/span&gt;
  &lt;span class="n"&gt;machine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_syscall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;93&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exit_handler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// exit&lt;/span&gt;

  &lt;span class="c1"&gt;// Allocate a 64-byte shared memory region on the host.&lt;/span&gt;
  &lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;shared_memory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;memset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shared_memory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0xFF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Map the shared memory into the machine's address space.&lt;/span&gt;
  &lt;span class="n"&gt;machine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;insert_memory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;reinterpret_cast&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;uintptr_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shared_memory&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Register a custom syscall (1002) for the guest to get the&lt;/span&gt;
  &lt;span class="c1"&gt;// virtual address of the shared memory.&lt;/span&gt;
  &lt;span class="n"&gt;machine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_syscall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1002&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;shared_memory&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;dawn&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;machine_t&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;machine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;machine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_registers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;machine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;translate_host_to_guest_virtual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;reinterpret_cast&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;uintptr_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shared_memory&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Load the guest ELF binary&lt;/span&gt;
  &lt;span class="n"&gt;machine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load_elf_and_set_program_counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="c1"&gt;// Start the simulation&lt;/span&gt;
  &lt;span class="n"&gt;machine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;simulate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// After simulation, print the shared memory to see guest's changes&lt;/span&gt;
  &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;cout&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s"&gt;"After machine:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;shared_memory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sc"&gt;'\n'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;machine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_registers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// Return guest's exit code&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Guest (C++) side, accessing the shared memory:&lt;/strong&gt;&lt;br&gt;
The guest code uses the same &lt;code&gt;define_syscall&lt;/code&gt; macro to create a C++ function for our custom syscall. It calls this function to get the pointer to the shared memory, writes a string into it, and then exits.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Guest-side example from tests/examples/simple/main.cpp&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;cstdint&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;cstring&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="c1"&gt;// This macro defines a wrapper for a syscall.&lt;/span&gt;
&lt;span class="cp"&gt;#define define_syscall(code, name, signature)                 \
  asm(".pushsection .text\n"                                  \
      ".func sys_" #name "\n"                                 \
      "sys_" #name ":\n"                                      \
      "   li a7, " #code "\n"                                 \
      "   ecall\n"                                            \
      "   ret\n"                                              \
      ".endfunc\n"                                            \
      ".popsection .text\n");                                 \
  using name##_t = signature;                                 \
  extern "C" __attribute__((used, retain)) void sys_##name(); \
  template &amp;lt;typename... args_t&amp;gt;                               \
  static inline auto name(args_t &amp;amp;&amp;amp;...args) {                 \
    auto fn = (name##_t *)sys_##name;                         \
    return fn(std::forward&amp;lt;args_t&amp;gt;(args)...);                 \
  }
&lt;/span&gt;
&lt;span class="c1"&gt;// Define the `get_mapped_memory` syscall (number 1002) which takes no arguments&lt;/span&gt;
&lt;span class="c1"&gt;// and returns a void pointer.&lt;/span&gt;
&lt;span class="n"&gt;define_syscall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1002&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;get_mapped_memory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Call the syscall to get the guest virtual address of the shared memory.&lt;/span&gt;
  &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;mapped_memory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;reinterpret_cast&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_mapped_memory&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="c1"&gt;// The string to be copied into the shared memory.&lt;/span&gt;
  &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"hello world, from riscv"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Copy the string into the shared memory. The host will see this change.&lt;/span&gt;
  &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;memcpy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mapped_memory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;strlen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Return 0 to indicate successful execution.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This same mechanism is used to provide a minimal C standard library (&lt;code&gt;newlib&lt;/code&gt;) implementation, handling &lt;code&gt;exit&lt;/code&gt;, memory allocation (&lt;code&gt;brk&lt;/code&gt;), and other essential functions.&lt;br&gt;
For a more complete look at host and guest code, take a look at simple &lt;a href="https://github.com/sivansh11/dawn/blob/main/examples/simple/main.cpp" rel="noopener noreferrer"&gt;host&lt;/a&gt; and simple &lt;a href="https://github.com/sivansh11/dawn/blob/main/tests/examples/simple/main.cpp" rel="noopener noreferrer"&gt;guest&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Compiling Guest Code
&lt;/h2&gt;

&lt;p&gt;To compile C/C++ code (or any other language) for our RISC-V emulator, we need a &lt;strong&gt;cross-compiler&lt;/strong&gt;. A cross-compiler runs on one architecture (e.g., x86-64 Linux) but generates executable code for a different architecture (e.g., RISC-V 64-bit).&lt;/p&gt;

&lt;p&gt;For RISC-V, the standard cross-compiler toolchain is typically prefixed with &lt;code&gt;riscv64-unknown-elf-&lt;/code&gt;. This indicates that it targets a 64-bit RISC-V architecture, for an "unknown" operating system (meaning it's a bare-metal or embedded target, without a full OS like Linux), and produces ELF-formatted executables.&lt;/p&gt;

&lt;p&gt;Currently, when compiling guest code, you must specify the instruction set architecture and application binary interface. Since the emulator currently only supports the base integer instruction set, the following flags are required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;-march&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;rv64i &lt;span class="nt"&gt;-mabi&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;lp64
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;-march=rv64i&lt;/code&gt;: Specifies the target architecture as RISC-V 64-bit with only the Integer (I) extension.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;-mabi=lp64&lt;/code&gt;: Specifies the Application Binary Interface (ABI) as LP64, which means &lt;code&gt;long&lt;/code&gt; and pointers are 64-bit, and &lt;code&gt;int&lt;/code&gt; is 32-bit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Getting the Cross-Compiler:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You can obtain the necessary RISC-V GNU Toolchain, which includes the &lt;code&gt;riscv64-unknown-elf-gcc&lt;/code&gt; cross-compiler, from the official GitHub repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/riscv-collab/riscv-gnu-toolchain" rel="noopener noreferrer"&gt;https://github.com/riscv-collab/riscv-gnu-toolchain&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Future Work
&lt;/h2&gt;

&lt;p&gt;This project is just getting started. Here are some of the next steps I have planned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Implement More Extensions:&lt;/strong&gt; The first priority is to implement the 'M' and 'F' extension for integer multiplication, division and floating point arithmetic, which is crucial for most programs.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Performance Optimizations:&lt;/strong&gt; I plan to explore more advanced interpreter designs, to speed up the instruction dispatch loop. Further down the line, a JIT compiler could be a possibility.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What are your thoughts?
&lt;/h3&gt;

&lt;p&gt;I'd love to hear your feedback, questions, or ideas in the comments below! If you found this article interesting, please consider sharing it.&lt;/p&gt;

&lt;p&gt;You can also check out the project's source code (if it's public) on &lt;a href="https://github.com/sivansh11/dawn" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>cpp</category>
      <category>assembly</category>
      <category>riscv</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
