Skip to content

[csharp] Verify codegen #738

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/csharp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ anyhow = { workspace = true }

[dev-dependencies]
test-helpers = { path = '../test-helpers' }

[features]
default = ["aot"]
aot = []
36 changes: 24 additions & 12 deletions crates/csharp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ impl Opts {
struct InterfaceFragment {
csharp_src: String,
csharp_interop_src: String,
stub: String,
}

pub struct InterfaceTypeAndFragments {
Expand Down Expand Up @@ -439,24 +440,32 @@ impl WorldGenerator for CSharp {

files.push(&format!("{name}.cs"), indent(&src).as_bytes());

let generate_stub = |name: String, files: &mut Files| {
let stub_file_name = format!("{name}Impl");
let interface_name = CSharp::get_class_name_from_qualified_name(name.clone());
let stub_class_name = format!("{interface_name}Impl");
let generate_stub =
|name: String, files: &mut Files, fragments: &Vec<InterfaceFragment>| {
let stub_file_name = format!("{name}Impl");
let interface_name = CSharp::get_class_name_from_qualified_name(name.clone());
let stub_class_name = format!("{interface_name}Impl");

let body = format!(
"// Generated by `wit-bindgen` {version}. DO NOT EDIT!
let body = fragments
.iter()
.map(|f| f.stub.deref())
.collect::<Vec<_>>()
.join("\n");

let body = format!(
"// Generated by `wit-bindgen` {version}. DO NOT EDIT!
{CSHARP_IMPORTS}

namespace {namespace}.{name};

public partial class {stub_class_name} : {interface_name} {{
{body}
}}
"
);
);

files.push(&format!("{stub_file_name}.cs"), indent(&body).as_bytes());
};
files.push(&format!("{stub_file_name}.cs"), indent(&body).as_bytes());
};

// TODO: is the world Impl class useful?
// if self.opts.generate_stub {
Expand Down Expand Up @@ -520,7 +529,7 @@ impl WorldGenerator for CSharp {
files.push(&format!("{name}Interop.cs"), indent(&body).as_bytes());

if interface_type_and_fragments.is_export && self.opts.generate_stub {
generate_stub(format!("{name}"), files);
generate_stub(format!("{name}"), files, fragments);
}
}
}
Expand Down Expand Up @@ -563,13 +572,15 @@ impl InterfaceGenerator<'_> {
.push(InterfaceFragment {
csharp_src: self.src,
csharp_interop_src: self.csharp_interop_src,
stub: self.stub,
});
}

fn add_world_fragment(self) {
self.gen.world_fragments.push(InterfaceFragment {
csharp_src: self.src,
csharp_interop_src: self.csharp_interop_src,
stub: self.stub,
});
}

Expand Down Expand Up @@ -975,7 +986,8 @@ impl InterfaceGenerator<'_> {
.collect::<Vec<_>>()
.join(", ");

format!("public static {result_type} {name}({params})")
let camel_case = name.to_upper_camel_case();
format!("public static {result_type} {camel_case}({params})")
}
}

Expand Down Expand Up @@ -1510,7 +1522,7 @@ impl Bindgen for FunctionBindgen<'_, '_> {
}
}

Instruction::Return { amt, func } => match func.results.len() {
Instruction::Return { amt: _, func } => match func.results.len() {
0 => (),
1 => uwriteln!(self.src, "return {};", operands[0]),
_ => {
Expand Down
173 changes: 170 additions & 3 deletions crates/csharp/tests/codegen.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// TODO: Implement tests similar to the other generators.
// This requires that we have any dependencies either included here or published to NuGet or similar.
use std::path::Path;
use std::{
env, fs,
path::{Path, PathBuf},
process::{Command, Stdio},
};
use wit_component::StringEncoding;

macro_rules! codegen_test {
Expand Down Expand Up @@ -31,6 +35,7 @@ macro_rules! codegen_test {
"lists",
"many-arguments",
"multi-return",
"multiversion",
"option-result",
"records",
"rename-interface",
Expand All @@ -47,11 +52,14 @@ macro_rules! codegen_test {
"result-empty",
"ret-areas",
"return-resource-from-export",
"same-names2",
"same-names5",
"simple-functions",
"simple-http",
"simple-lists",
"small-anonymous",
"smoke-default",
"strings",
"unused-import",
"use-across-interfaces",
"variants",
Expand All @@ -77,6 +85,165 @@ macro_rules! codegen_test {
}
test_helpers::codegen_tests!();

fn verify(_dir: &Path, _name: &str) {
// TODO?
fn verify(dir: &Path, name: &str) {
#[cfg(all(target_os = "windows", feature = "aot"))]
aot_verify(dir, name);
}

fn aot_verify(dir: &Path, name: &str) {
let mut wasm_filename = dir.join(name);
wasm_filename.set_extension("wasm");

fs::write(
dir.join("nuget.config"),
r#"<?xml version="1.0" encoding="utf-8"?>
<configuration>
<config>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this is copied from the runtime version. I should have removed this really as I don't think there is much benefit and it may improve the CI time without it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its fine though and we can address them both at the same time.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there a bit of duplicate code and this will become more challenging with a different project settings required for mono work. Let's follow up with a clean up?

<add key="globalPackagesFolder" value=".packages" />
</config>
<packageSources>
<!--To inherit the global NuGet package sources remove the <clear/> line below -->
<clear />
<add key="nuget" value="https://api.nuget.org/v3/index.json" />
<add key="dotnet-experimental" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-experimental/nuget/v3/index.json" />
<!--<add key="dotnet-experimental" value="C:\github\runtimelab\artifacts\packages\Debug\Shipping" />-->
</packageSources>
</configuration>"#,
).unwrap();

fs::write(
dir.join("rd.xml"),
format!(
r#"<Directives xmlns="http://schemas.microsoft.com/netfx/2013/01/metadata">
<Application>
<Assembly Name="{name}">
</Assembly>
</Application>
</Directives>"#
),
)
.unwrap();

let mut csproj = format!(
"<Project Sdk=\"Microsoft.NET.Sdk\">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>preview</LangVersion>
<RootNamespace>{name}</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<PropertyGroup>
<PublishTrimmed>true</PublishTrimmed>
<AssemblyName>{name}</AssemblyName>
</PropertyGroup>
"
);

csproj.push_str(
r#"
<ItemGroup>
<RdXmlFile Include="rd.xml" />
</ItemGroup>

"#,
);

csproj.push_str("\t<ItemGroup>\n");
csproj.push_str(&format!(
"\t\t<NativeLibrary Include=\"the_world_component_type.o\" />\n"
));
csproj.push_str("\t</ItemGroup>\n\n");

csproj.push_str(
r#"
<ItemGroup>
<CustomLinkerArg Include="-Wl,--export,_initialize" />
<CustomLinkerArg Include="-Wl,--no-entry" />
<CustomLinkerArg Include="-mexec-model=reactor" />
</ItemGroup>
"#,
);

// In CI we run out of disk space if we don't clean up the files, we don't need to keep any of it around.
csproj.push_str(&format!(
"<Target Name=\"CleanAndDelete\" AfterTargets=\"Clean\">
<!-- Remove obj folder -->
<RemoveDir Directories=\"$(BaseIntermediateOutputPath)\" />
<!-- Remove bin folder -->
<RemoveDir Directories=\"$(BaseOutputPath)\" />
<RemoveDir Directories=\"{}\" />
<RemoveDir Directories=\".packages\" />
</Target>

",
wasm_filename.display()
));

csproj.push_str(
r#"
<ItemGroup>
<PackageReference Include="Microsoft.DotNet.ILCompiler.LLVM" Version="8.0.0-*" />
<PackageReference Include="runtime.win-x64.Microsoft.DotNet.ILCompiler.LLVM" Version="8.0.0-*" />
</ItemGroup>
</Project>
"#,
);

fs::write(dir.join(format!("{name}.csproj")), csproj).unwrap();

let dotnet_root_env = "DOTNET_ROOT";
let dotnet_cmd: PathBuf;
match env::var(dotnet_root_env) {
Ok(val) => dotnet_cmd = Path::new(&val).join("dotnet"),
Err(_e) => dotnet_cmd = "dotnet".into(),
}

let mut cmd = Command::new(dotnet_cmd.clone());

cmd.current_dir(&dir);

// add .arg("/bl") to diagnose dotnet build problems
cmd.arg("build")
.arg(dir.join(format!("{name}.csproj")))
.arg("-r")
.arg("wasi-wasm")
.arg("-c")
.arg("Debug")
.arg("/p:PlatformTarget=AnyCPU")
.arg("/p:MSBuildEnableWorkloadResolver=false")
.arg("--self-contained")
.arg("/p:UseAppHost=false")
.arg("-o")
.arg(&wasm_filename);
let output = match cmd.output() {
Ok(output) => output,
Err(e) => panic!("failed to spawn compiler: {}", e),
};

if !output.status.success() {
println!("status: {}", output.status);
println!("stdout: ------------------------------------------");
println!("{}", String::from_utf8_lossy(&output.stdout));
println!("stderr: ------------------------------------------");
println!("{}", String::from_utf8_lossy(&output.stderr));
panic!("failed to compile");
}

let mut cmd = Command::new(dotnet_cmd);
match cmd
.stdout(Stdio::null())
.current_dir(&dir)
.arg("clean")
.spawn()
{
Err(e) => println!(
"failed to clean project which may cause disk pressure in CI. {}",
e
),
_ => {}
}
}