oo_bindgen/backend/dotnet/
mod.rs

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
26/// Map from Rust platform to a .NET platform string
27///
28/// Packages not in this map will cause an error
29fn dotnet_platform_string(platform: &Platform) -> Option<&'static str> {
30    // Names taken from https://docs.microsoft.com/en-us/dotnet/core/rid-catalog
31    match *platform {
32        // Windows targets
33        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        // OSX targets
37        platform::X86_64_APPLE_DARWIN => Some("osx-x64"),
38        platform::AARCH64_APPLE_DARWIN => Some("osx-arm64"),
39        // Linux GLIBC targets
40        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        // Linux MUSL targets
44        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        // other targets just use the target triple
48        _ => None,
49    }
50}
51
52/// Target framework - affects runtime compatible and allowed language features
53///
54/// Default C# versions for different targets specified here:
55///
56/// <https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/configure-language-version>
57///
58#[derive(Debug, Copy, Clone, clap::ValueEnum)]
59pub(crate) enum TargetFramework {
60    /// .NET Standard 2.0 - Compatible with .NET Framework 4.6.1 -> 4.8
61    /// Defaults to C# 7.3
62    NetStandard2_0,
63    /// .NET Standard 2.1 - NOT compatible with any .NET Framework
64    /// Defaults to C# 8.0
65    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 the helper classes
118    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    // Open file
139    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>")?; // Include symbols
153    f.writeln("    <SymbolPackageFormat>snupkg</SymbolPackageFormat>")?; // Use new file format
154    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    // Include each compiled FFI lib
193    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    // Include the target files to force the copying of DLLs of NuGet packages on .NET Framework
202    // See https://github.com/stepfunc/dnp3/issues/147
203    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    // Dependencies and files to include
209    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    // The target file is used to automatically copy the DLL to the build directory when using
230    // .NET Framework (Windows only). In .NET Core or .NET 5/6, the DLLs are automatically
231    // loaded from the appropriate runtime/*/native directory.
232    // This solution is based on gRPC library.
233    // We only support x64 Platform and .NET Framework 4.5 or higher.
234    // See https://github.com/stepfunc/dnp3/issues/147
235
236    // Main target file
237    {
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    // Transistive target file (simply points to the main one)
268    {
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        // Open file
303        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        // Open file
317        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        // Open file
331        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        // Open file
345        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            // Print top-level documentation
380            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            // Print top-level documentation
411            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            // Print top-level documentation
441            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        // Open file
467        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        // Open file
477        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        // Open file
491        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        // Open file
505        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        // Open file
522        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    // Copy doxygen awesome in target directory
551    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    // Write the logo file
558    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            // Output customization
568            "GENERATE_LATEX = NO",       // No LaTeX
569            "HIDE_UNDOC_CLASSES = YES",  // I guess this will help the output
570            "ALWAYS_DETAILED_SEC = YES", // Always print detailed section
571            "AUTOLINK_SUPPORT = NO",     // Only link when we explicitly want to
572            // Styling
573            "HTML_EXTRA_STYLESHEET = doxygen-awesome.css",
574            "GENERATE_TREEVIEW = YES",
575            "PROJECT_LOGO = logo.png",
576            "HTML_COLORSTYLE_HUE = 209", // See https://jothepro.github.io/doxygen-awesome-css/index.html#autotoc_md14
577            "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}