Expand description
§inline_csharp
Embed C# directly in Rust — evaluated at program runtime (csharp!, csharp_fn!) or at
compile time (ct_csharp!).
§Prerequisites
.NET 8 (or later) SDK with dotnet on PATH.
§Quick start
# Cargo.toml
[dependencies]
inline_csharp = "0.1.0"§csharp! — runtime, no parameters
Compiles and runs C# each time the surrounding Rust code executes. Expands
to Result<T, inline_csharp::CsharpError>.
use inline_csharp::csharp;
// No type annotation needed — the macro infers `i32` from `static int Run()`
let x = csharp! {
static int Run() {
return 42;
}
}.unwrap();§csharp_fn! — runtime, with parameters
Like csharp!, but Run(...) may declare parameters. Expands to a Rust
function value fn(P1, P2, …) -> Result<T, CsharpError>. Parameters are
serialised by Rust and piped to the C# process over stdin.
use inline_csharp::csharp_fn;
// Single parameter — return type inferred from `static int Run()`
let doubled = csharp_fn! {
static int Run(int n) {
return n * 2;
}
}(21).unwrap();
// Multiple parameters
let msg: String = csharp_fn! {
static string Run(string greeting, string target) {
return greeting + ", " + target + "!";
}
}("Hello", "World").unwrap();
// Nullable parameter
let result: Option<i32> = csharp_fn! {
static int? Run(int? val) {
return val.HasValue ? val * 2 : null;
}
}(Some(21)).unwrap();§ct_csharp! — compile time
Runs C# during rustc macro expansion and splices the result as a Rust
literal at the call site. No parameters are allowed (values must be
compile-time constants).
use inline_csharp::ct_csharp;
const PI: f64 = ct_csharp! {
static double Run() {
return System.Math.PI;
}
};
// Arrays work too — result is a Rust array literal baked into the binary
const PRIMES: [i32; 5] = ct_csharp! {
static int[] Run() {
return new int[] { 2, 3, 5, 7, 11 };
}
};§Supported parameter types (csharp_fn!)
Declare parameters in the C# Run(...) signature; Rust receives them with
the mapped types below.
| C# parameter type | Rust parameter type |
|---|---|
sbyte | i8 |
byte | u8 |
short | i16 |
ushort | u16 |
int | i32 |
uint | u32 |
long | i64 |
ulong | u64 |
float | f32 |
double | f64 |
bool | bool |
char | char |
string | &str |
T[] / List<T> | &[T] |
T? | Option<T> |
§Supported return types
| C# return type | Rust return type |
|---|---|
sbyte | i8 |
byte | u8 |
short | i16 |
ushort | u16 |
int | i32 |
uint | u32 |
long | i64 |
ulong | u64 |
float | f32 |
double | f64 |
bool | bool |
char | char |
string | String |
T[] / List<T> | Vec<T> |
T? | Option<T> |
Types can be nested arbitrarily: List<string>[] → Vec<Vec<String>>,
int?[] → Vec<Option<i32>>, etc.
§Options
The following optional key = "value" pairs may appear before the C# body, separated by
commas:
build = "<args>"— extra arguments passed todotnet build.run = "<args>"— extra arguments passed todotnet <dll>at runtime.reference = "<path>"— path to a DLL to reference (repeatable).
use inline_csharp::csharp;
let result: i32 = csharp! {
build = "--no-restore",
reference = "../../libs/Foo.dll",
static int Run() {
return Foo.Value;
}
}.unwrap();§Cache directory
Compiled assemblies are cached so that unchanged C# code is not recompiled on every run. The cache root is resolved in this order:
| Priority | Location |
|---|---|
| 1 | INLINE_CSHARP_CACHE_DIR environment variable (if set and non-empty) |
| 2 | Platform cache directory — ~/.cache/inline_csharp on Linux, ~/Library/Caches/inline_csharp on macOS, %LOCALAPPDATA%\inline_csharp on Windows |
| 3 | <system temp>/inline_csharp (fallback if the platform cache dir is unavailable) |
Each compiled assembly gets its own subdirectory named
<ClassName>_<hash>/, where the hash covers the C# source, the
expanded build flags, the current working directory, the raw run
flags, the reference DLL paths, and the maximum modification time of
any .cs, .dll, .nupkg, or .zip file found under directories
(or individual files) listed in reference. This means rebuilding a
referenced library automatically triggers a fresh compilation.
To force recompilation regardless of the hash (e.g. after editing files
outside the tracked paths), set INLINE_CSHARP_CACHE_INVALIDATE=true
(also accepts 1 or yes). This removes the cache entry for the
current class before compiling, so a clean build always runs:
INLINE_CSHARP_CACHE_INVALIDATE=true cargo run§Using project C# source files / namespaces
Use using or namespace directives together with reference = "..." to call into your own C# code:
use inline_csharp::csharp;
// using style
let s: String = csharp! {
using MyNamespace;
static string Run() {
return new MyClass().Greet();
}
}.unwrap();use inline_csharp::csharp;
// namespace style — the generated class becomes part of the named namespace
let s: String = csharp! {
namespace MyNamespace;
static string Run() {
return new MyClass().Greet();
}
}.unwrap();§Refactoring use case
inline_csharp is particularly well-suited for incremental C# → Rust
migrations. The typical workflow is:
- Keep the original C# logic intact.
- Write the replacement in Rust.
- Use
csharp_fn!to call the original C# with the same inputs and assert that both implementations produce identical outputs.
use inline_csharp::csharp_fn;
fn my_rust_impl(n: i32) -> i32 {
// … new Rust code …
n * 2
}
fn parity_with_csharp() {
let csharp_impl = csharp_fn! {
static int Run(int n) {
// original C# logic, verbatim
return n * 2;
}
};
for n in [0, 1, -1, 42, i32::MAX / 2] {
let expected = csharp_impl(n).unwrap();
assert_eq!(my_rust_impl(n), expected, "diverged for n={n}");
}
}
parity_with_csharp();§Crate layout
| Crate | Purpose |
|---|---|
inline_csharp | Public API — re-exports macros and core types |
inline_csharp_macros | Proc-macro implementation (csharp!, csharp_fn!, ct_csharp!) |
inline_csharp_core | Runtime helpers (run_csharp, CsharpError) |
inline_csharp_demo | Demo binary |
Macros§
- csharp
- Re-export the proc macros so users only need to depend on this crate. Compile and run zero-argument C# code at program runtime.
- csharp_
fn - Re-export the proc macros so users only need to depend on this crate. Return a typed Rust function that compiles and runs C# at program runtime.
- ct_
csharp - Re-export the proc macros so users only need to depend on this crate. Run C# at compile time and splice its return value as a Rust literal.
Enums§
- Csharp
Error - Re-export the core error type and runtime helpers.
All errors that
csharp!andcsharp_fn!can return at runtime (and thatct_csharp!maps tocompile_error!diagnostics at build time).
Functions§
- expand_
dotnet_ args - Re-export the core error type and runtime helpers.
Shell-expand
raw(expanding env vars and~), then split into individual arguments (respecting quotes). Returns an empty vec ifrawis empty. - run_
csharp - Re-export the core error type and runtime helpers. Compile (if needed) and run a generated C# class, returning raw stdout bytes.