1use std::fmt::Formatter;
2use std::fs;
3use std::io::Write;
4use std::path::Path;
5use std::path::PathBuf;
6use std::process::Command;
7
8use crate::backend::*;
9use crate::model::*;
10
11use conversion::*;
12use doc::*;
13use formatting::*;
14
15mod class;
16mod conversion;
17mod doc;
18mod formatting;
19mod helpers;
20mod interface;
21mod structure;
22mod wrappers;
23
24pub(crate) const NATIVE_FUNCTIONS_CLASSNAME: &str = "NativeFunctions";
25
26fn dotnet_platform_string(platform: &Platform) -> Option<&'static str> {
30 match *platform {
32 platform::X86_64_PC_WINDOWS_MSVC => Some("win-x64"),
34 platform::I686_PC_WINDOWS_MSVC => Some("win-x86"),
35 platform::AARCH64_PC_WINDOWS_MSVC => Some("win-arm64"),
36 platform::X86_64_APPLE_DARWIN => Some("osx-x64"),
38 platform::AARCH64_APPLE_DARWIN => Some("osx-arm64"),
39 platform::X86_64_UNKNOWN_LINUX_GNU => Some("linux-x64"),
41 platform::AARCH64_UNKNOWN_LINUX_GNU => Some("linux-arm64"),
42 platform::ARM_UNKNOWN_LINUX_GNUEABIHF => Some("linux-arm"),
43 platform::X86_64_UNKNOWN_LINUX_MUSL => Some("linux-musl-x64"),
45 platform::AARCH64_UNKNOWN_LINUX_MUSL => Some("linux-musl-arm64"),
46 platform::ARM_UNKNOWN_LINUX_MUSLEABIHF => Some("linux-musl-arm"),
47 _ => None,
49 }
50}
51
52#[derive(Debug, Copy, Clone, clap::ValueEnum)]
59pub(crate) enum TargetFramework {
60 NetStandard2_0,
63 NetStandard2_1,
66}
67
68impl std::fmt::Display for TargetFramework {
69 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
70 write!(f, "{self:?}")
71 }
72}
73
74impl TargetFramework {
75 pub(crate) fn get_target_framework_str(&self) -> &'static str {
76 match self {
77 TargetFramework::NetStandard2_0 => "netstandard2.0",
78 TargetFramework::NetStandard2_1 => "netstandard2.1",
79 }
80 }
81
82 pub(crate) fn supports_default_interface_methods(&self) -> bool {
83 match self {
84 TargetFramework::NetStandard2_0 => false,
85 TargetFramework::NetStandard2_1 => true,
86 }
87 }
88}
89
90pub(crate) struct DotnetBindgenConfig {
91 pub(crate) output_dir: PathBuf,
92 pub(crate) ffi_name: &'static str,
93 pub(crate) extra_files: Vec<PathBuf>,
94 pub(crate) platforms: PlatformLocations,
95 pub(crate) generate_doxygen: bool,
96 pub(crate) target_framework: TargetFramework,
97}
98
99pub(crate) fn generate_dotnet_bindings(
100 lib: &Library,
101 config: &DotnetBindgenConfig,
102) -> FormattingResult<()> {
103 logged::create_dir_all(&config.output_dir)?;
104
105 generate_csproj(lib, config)?;
106 generate_targets_scripts(lib, config)?;
107 generate_native_functions(lib, config)?;
108 generate_constants(lib, config)?;
109 generate_structs(lib, config)?;
110 generate_enums(lib, config)?;
111 generate_exceptions(lib, config)?;
112 generate_classes(lib, config)?;
113 generate_interfaces(lib, config)?;
114 generate_collection_helpers(lib, config)?;
115 generate_iterator_helpers(lib, config)?;
116
117 generate_helpers(lib, config)?;
119
120 if config.generate_doxygen {
121 generate_doxygen(lib, config)?;
122 }
123
124 Ok(())
125}
126
127fn generate_helpers(lib: &Library, config: &DotnetBindgenConfig) -> FormattingResult<()> {
128 let mut filename = config.output_dir.clone();
129 filename.push("Helpers");
130 filename.set_extension("cs");
131 let mut f = FilePrinter::new(filename)?;
132
133 print_license(&mut f, &lib.info.license_description)?;
134 f.writeln(include_str!("../../../static/dotnet/Helpers.cs"))
135}
136
137fn generate_csproj(lib: &Library, config: &DotnetBindgenConfig) -> FormattingResult<()> {
138 let mut filename = config.output_dir.clone();
140 filename.push(lib.settings.name.to_string());
141 filename.set_extension("csproj");
142 let mut f = FilePrinter::new(filename)?;
143
144 f.writeln("<Project Sdk=\"Microsoft.NET.Sdk\">")?;
145 f.writeln(" <PropertyGroup>")?;
146 f.writeln(&format!(
147 " <TargetFramework>{}</TargetFramework>",
148 config.target_framework.get_target_framework_str()
149 ))?;
150
151 f.writeln(" <GenerateDocumentationFile>true</GenerateDocumentationFile>")?;
152 f.writeln(" <IncludeSymbols>true</IncludeSymbols>")?; f.writeln(" <SymbolPackageFormat>snupkg</SymbolPackageFormat>")?; f.writeln(&format!(" <PackageId>{}</PackageId>", lib.settings.name))?;
155 f.writeln(&format!(
156 " <PackageVersion>{}</PackageVersion>",
157 lib.version
158 ))?;
159 f.writeln(&format!(
160 "<VersionPrefix>{}.{}.{}</VersionPrefix>",
161 lib.version.major, lib.version.minor, lib.version.patch
162 ))?;
163
164 if !lib.version.pre.is_empty() {
165 f.writeln(&format!(
166 "<VersionSuffix>{}</VersionSuffix>",
167 lib.version.pre
168 ))?;
169 }
170
171 f.writeln(&format!(
172 " <Description>{}</Description>",
173 lib.info.description
174 ))?;
175 f.writeln(&format!(
176 " <PackageProjectUrl>{}</PackageProjectUrl>",
177 lib.info.project_url
178 ))?;
179 f.writeln(&format!(
180 " <RepositoryUrl>https://github.com/{}.git</RepositoryUrl>",
181 lib.info.repository
182 ))?;
183 f.writeln(" <RepositoryType>git</RepositoryType>")?;
184 f.writeln(&format!(
185 " <PackageLicenseFile>{}</PackageLicenseFile>",
186 lib.info.license_path.file_name().unwrap().to_string_lossy()
187 ))?;
188 f.writeln(" </PropertyGroup>")?;
189 f.newline()?;
190 f.writeln(" <ItemGroup>")?;
191
192 for p in config.platforms.iter() {
194 let ps = dotnet_platform_string(&p.platform)
195 .unwrap_or_else(|| panic!("No RID mapped for Rust target: {}", p.platform));
196 let filename = p.platform.bin_filename(config.ffi_name);
197 let filepath = dunce::canonicalize(p.location.join(&filename))?;
198 f.writeln(&format!(" <Content Include=\"{}\" Link=\"{}\" Pack=\"true\" PackagePath=\"runtimes/{}/native\" CopyToOutputDirectory=\"PreserveNewest\" />", filepath.to_string_lossy(), filename, ps))?;
199 }
200
201 f.writeln(&format!(" <Content Include=\"build/net45/{}.targets\" Pack=\"true\" PackagePath=\"build/net45/\" />", lib.settings.name))?;
204 f.writeln(&format!(" <Content Include=\"buildTransitive/net45/{}.targets\" Pack=\"true\" PackagePath=\"buildTransitive/net45/\" />", lib.settings.name))?;
205
206 f.writeln(" </ItemGroup>")?;
207
208 f.writeln(" <ItemGroup>")?;
210 f.writeln(
211 " <PackageReference Include=\"System.Collections.Immutable\" Version=\"1.7.1\" />",
212 )?;
213 f.writeln(&format!(
214 " <None Include=\"{}\" Pack=\"true\" PackagePath=\"\" />",
215 dunce::canonicalize(&lib.info.license_path)?.to_string_lossy()
216 ))?;
217 for path in &config.extra_files {
218 f.writeln(&format!(
219 " <None Include=\"{}\" Pack=\"true\" PackagePath=\"\" />",
220 dunce::canonicalize(path)?.to_string_lossy()
221 ))?;
222 }
223 f.writeln(" </ItemGroup>")?;
224
225 f.writeln("</Project>")
226}
227
228fn generate_targets_scripts(lib: &Library, config: &DotnetBindgenConfig) -> FormattingResult<()> {
229 {
238 let mut filename = config.output_dir.clone();
239 filename.push("build");
240 filename.push("net45");
241
242 fs::create_dir_all(&filename)?;
243
244 filename.push(lib.settings.name.to_string());
245 filename.set_extension("targets");
246 let mut f = FilePrinter::new(filename)?;
247
248 f.writeln("<?xml version=\"1.0\" encoding=\"utf-8\"?>")?;
249 f.writeln("<Project ToolsVersion=\"4.0\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">")?;
250 f.writeln(" <ItemGroup>")?;
251
252 for p in config.platforms.iter() {
253 if let Some(ps) = dotnet_platform_string(&p.platform) {
254 if p.platform.target_os == OS::Windows && p.platform.target_arch == Arch::X86_64 {
255 f.writeln(&format!(" <Content Condition=\"'$(Platform)' == 'x64'\" Include=\"$(MSBuildThisFileDirectory)../../runtimes/{}/native/{}\" Link=\"{}\" CopyToOutputDirectory=\"Always\" Visible=\"false\" NuGetPackageId=\"{}\" />", ps, p.platform.bin_filename(config.ffi_name), p.platform.bin_filename(config.ffi_name), lib.settings.name))?;
256 } else if p.platform.target_os == OS::Windows && p.platform.target_arch == Arch::X86
257 {
258 f.writeln(&format!(" <Content Condition=\"'$(Platform)' == 'x86'\" Include=\"$(MSBuildThisFileDirectory)../../runtimes/{}/native/{}\" Link=\"{}\" CopyToOutputDirectory=\"Always\" Visible=\"false\" NuGetPackageId=\"{}\" />", ps, p.platform.bin_filename(config.ffi_name), p.platform.bin_filename(config.ffi_name), lib.settings.name))?;
259 }
260 }
261 }
262
263 f.writeln(" </ItemGroup>")?;
264 f.writeln("</Project>")?;
265 }
266
267 {
269 let mut filename = config.output_dir.clone();
270 filename.push("buildTransitive");
271 filename.push("net45");
272
273 fs::create_dir_all(&filename)?;
274
275 filename.push(lib.settings.name.to_string());
276 filename.set_extension("targets");
277 let mut f = FilePrinter::new(filename)?;
278
279 f.writeln("<?xml version=\"1.0\" encoding=\"utf-8\"?>")?;
280 f.writeln("<Project ToolsVersion=\"4.0\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">")?;
281 f.writeln(&format!(
282 " <Import Project=\"$(MSBuildThisFileDirectory)../../build/net45/{}.targets\" />",
283 lib.settings.name
284 ))?;
285 f.writeln("</Project>")?;
286 }
287
288 Ok(())
289}
290
291fn generate_native_functions(lib: &Library, config: &DotnetBindgenConfig) -> FormattingResult<()> {
292 let mut filename = config.output_dir.clone();
293 filename.push(NATIVE_FUNCTIONS_CLASSNAME);
294 filename.set_extension("cs");
295 let mut f = FilePrinter::new(filename)?;
296
297 wrappers::generate_native_functions_class(&mut f, lib, config)
298}
299
300fn generate_constants(lib: &Library, config: &DotnetBindgenConfig) -> FormattingResult<()> {
301 for constants in lib.constants() {
302 let mut filename = config.output_dir.clone();
304 filename.push(constants.name.to_string());
305 filename.set_extension("cs");
306 let mut f = FilePrinter::new(filename)?;
307
308 generate_constant_set(&mut f, constants, lib)?;
309 }
310
311 Ok(())
312}
313
314fn generate_structs(lib: &Library, config: &DotnetBindgenConfig) -> FormattingResult<()> {
315 for st in lib.structs() {
316 let mut filename = config.output_dir.clone();
318 filename.push(st.name().camel_case());
319 filename.set_extension("cs");
320 let mut f = FilePrinter::new(filename)?;
321
322 structure::generate(&mut f, lib, st)?;
323 }
324
325 Ok(())
326}
327
328fn generate_enums(lib: &Library, config: &DotnetBindgenConfig) -> FormattingResult<()> {
329 for native_enum in lib.enums() {
330 let mut filename = config.output_dir.clone();
332 filename.push(native_enum.name.camel_case());
333 filename.set_extension("cs");
334 let mut f = FilePrinter::new(filename)?;
335
336 generate_enum(&mut f, native_enum, lib)?;
337 }
338
339 Ok(())
340}
341
342fn generate_exceptions(lib: &Library, config: &DotnetBindgenConfig) -> FormattingResult<()> {
343 for err in lib.error_types() {
344 let mut filename = config.output_dir.clone();
346 filename.push(err.exception_name.camel_case());
347 filename.set_extension("cs");
348 let mut f = FilePrinter::new(filename)?;
349
350 generate_exception(&mut f, err, lib)?;
351 }
352
353 Ok(())
354}
355
356fn generate_constant_set(
357 f: &mut impl Printer,
358 set: &Handle<ConstantSet<Validated>>,
359 lib: &Library,
360) -> FormattingResult<()> {
361 fn get_type_as_string(value: &ConstantValue) -> &'static str {
362 match value {
363 ConstantValue::U8(_, _) => "byte",
364 }
365 }
366
367 fn get_value_as_string(value: &ConstantValue) -> String {
368 match value {
369 ConstantValue::U8(x, Representation::Hex) => format!("0x{x:02X?}"),
370 }
371 }
372
373 print_license(f, &lib.info.license_description)?;
374 print_imports(f)?;
375 f.newline()?;
376
377 namespaced(f, &lib.settings.name, |f| {
378 documentation(f, |f| {
379 xmldoc_print(f, &set.doc)
381 })?;
382
383 f.writeln(&format!("public static class {}", set.name.camel_case()))?;
384 blocked(f, |f| {
385 for value in &set.values {
386 documentation(f, |f| xmldoc_print(f, &value.doc))?;
387 f.writeln(&format!(
388 "public const {} {} = {};",
389 get_type_as_string(&value.value),
390 value.name.camel_case(),
391 get_value_as_string(&value.value),
392 ))?;
393 }
394 Ok(())
395 })
396 })
397}
398
399fn generate_enum(
400 f: &mut impl Printer,
401 native_enum: &Handle<Enum<Validated>>,
402 lib: &Library,
403) -> FormattingResult<()> {
404 print_license(f, &lib.info.license_description)?;
405 print_imports(f)?;
406 f.newline()?;
407
408 namespaced(f, &lib.settings.name, |f| {
409 documentation(f, |f| {
410 xmldoc_print(f, &native_enum.doc)
412 })?;
413
414 f.writeln(&format!("public enum {}", native_enum.name.camel_case()))?;
415 blocked(f, |f| {
416 for variant in &native_enum.variants {
417 documentation(f, |f| xmldoc_print(f, &variant.doc))?;
418 f.writeln(&format!(
419 "{} = {},",
420 variant.name.camel_case(),
421 variant.value
422 ))?;
423 }
424 Ok(())
425 })
426 })
427}
428
429fn generate_exception(
430 f: &mut impl Printer,
431 err: &ErrorType<Validated>,
432 lib: &Library,
433) -> FormattingResult<()> {
434 print_license(f, &lib.info.license_description)?;
435 print_imports(f)?;
436 f.newline()?;
437
438 namespaced(f, &lib.settings.name, |f| {
439 documentation(f, |f| {
440 xmldoc_print(f, &err.inner.doc)
442 })?;
443
444 let error_name = err.inner.name.camel_case();
445 let exception_name = err.exception_name.camel_case();
446
447 f.writeln(&format!("public class {exception_name}: Exception"))?;
448 blocked(f, |f| {
449 documentation(f, |f| {
450 f.writeln("<summary>")?;
451 f.write("Error detail")?;
452 f.write("</summary>")
453 })?;
454 f.writeln(&format!("public readonly {error_name} error;"))?;
455 f.newline()?;
456 f.writeln(&format!(
457 "internal {exception_name}({error_name} error) : base(error.ToString())"
458 ))?;
459 blocked(f, |f| f.writeln("this.error = error;"))
460 })
461 })
462}
463
464fn generate_classes(lib: &Library, config: &DotnetBindgenConfig) -> FormattingResult<()> {
465 for class in lib.classes() {
466 let mut filename = config.output_dir.clone();
468 filename.push(class.name().camel_case());
469 filename.set_extension("cs");
470 let mut f = FilePrinter::new(filename)?;
471
472 class::generate(&mut f, class, lib)?;
473 }
474
475 for class in lib.static_classes() {
476 let mut filename = config.output_dir.clone();
478 filename.push(class.name.camel_case());
479 filename.set_extension("cs");
480 let mut f = FilePrinter::new(filename)?;
481
482 class::generate_static(&mut f, class, lib)?;
483 }
484
485 Ok(())
486}
487
488fn generate_interfaces(lib: &Library, config: &DotnetBindgenConfig) -> FormattingResult<()> {
489 for interface in lib.interfaces() {
490 let mut filename = config.output_dir.clone();
492 filename.push(format!("I{}", interface.name().camel_case()));
493 filename.set_extension("cs");
494 let mut f = FilePrinter::new(filename)?;
495
496 interface::generate(&mut f, interface, lib, config.target_framework)?;
497 }
498
499 Ok(())
500}
501
502fn generate_iterator_helpers(lib: &Library, config: &DotnetBindgenConfig) -> FormattingResult<()> {
503 for iter in lib.iterators() {
504 let mut filename = config.output_dir.clone();
506 filename.push(format!("{}Helpers", iter.name().camel_case()));
507 filename.set_extension("cs");
508 let mut f = FilePrinter::new(filename)?;
509
510 helpers::generate_iterator_helpers(&mut f, iter, lib)?;
511 }
512
513 Ok(())
514}
515
516fn generate_collection_helpers(
517 lib: &Library,
518 config: &DotnetBindgenConfig,
519) -> FormattingResult<()> {
520 for coll in lib.collections() {
521 let mut filename = config.output_dir.clone();
523 filename.push(format!("{}Helpers", coll.name().camel_case()));
524 filename.set_extension("cs");
525 let mut f = FilePrinter::new(filename)?;
526
527 helpers::generate_collection_helpers(&mut f, coll, lib)?;
528 }
529
530 Ok(())
531}
532
533fn print_license(f: &mut dyn Printer, license: &[String]) -> FormattingResult<()> {
534 commented(f, |f| {
535 for line in license.iter() {
536 f.writeln(line)?;
537 }
538 Ok(())
539 })
540}
541
542fn print_imports(f: &mut dyn Printer) -> FormattingResult<()> {
543 f.writeln("using System;")?;
544 f.writeln("using System.Runtime.InteropServices;")?;
545 f.writeln("using System.Threading.Tasks;")?;
546 f.writeln("using System.Collections.Immutable;")
547}
548
549fn generate_doxygen(lib: &Library, config: &DotnetBindgenConfig) -> FormattingResult<()> {
550 let doxygen_awesome = include_str!("../../../static/doxygen-awesome.css");
552 fs::write(
553 config.output_dir.join("doxygen-awesome.css"),
554 doxygen_awesome,
555 )?;
556
557 fs::write(config.output_dir.join("logo.png"), lib.info.logo_png)?;
559
560 run_doxygen(
561 &config.output_dir,
562 &[
563 &format!("PROJECT_NAME = {} (.NET API)", lib.settings.name),
564 &format!("PROJECT_NUMBER = {}", lib.version),
565 "INPUT = ./",
566 "HTML_OUTPUT = doc",
567 "GENERATE_LATEX = NO", "HIDE_UNDOC_CLASSES = YES", "ALWAYS_DETAILED_SEC = YES", "AUTOLINK_SUPPORT = NO", "HTML_EXTRA_STYLESHEET = doxygen-awesome.css",
574 "GENERATE_TREEVIEW = YES",
575 "PROJECT_LOGO = logo.png",
576 "HTML_COLORSTYLE_HUE = 209", "HTML_COLORSTYLE_SAT = 255",
578 "HTML_COLORSTYLE_GAMMA = 113",
579 ],
580 )?;
581
582 Ok(())
583}
584
585fn run_doxygen(cwd: &Path, config_lines: &[&str]) -> FormattingResult<()> {
586 let mut command = Command::new("doxygen")
587 .current_dir(cwd)
588 .arg("-")
589 .stdin(std::process::Stdio::piped())
590 .spawn()?;
591
592 {
593 let stdin = command.stdin.as_mut().unwrap();
594
595 for line in config_lines {
596 stdin.write_all(&format!("{line}\n").into_bytes())?;
597 }
598 }
599
600 command.wait()?;
601
602 Ok(())
603}