Repository: dtolnay/cxx (language: rust)

Issue:
Use /// when propagating doc comments
In #616 I've noticed `//` is used for the comments; using `///` instead will allow Doxygen to recognize them as doc comments.


Discussion / hints:
It seems not the case. We have some docstrings in the rust part that are indeed as `///` but they are "copied" in the generate files as `//` instead of the format understood by Doxygen [doc](https://www.doxygen.nl/manual/docblocks.html)

It can be useful to have them "copied" with the right format so that Doxygen can be used to generate docs for the bindings.
@dtolnay regarding https://github.com/dtolnay/cxx/pull/1048#discussion_r1020063842
> I understand that it could be simple for someone to work around, but I am not interested in accepting this PR as currently implemented. Taking Rust docs and turning them into either doxygen docs or silently into something that looks like doxygen docs, but isn't, is going to come across as clearly broken behavior to anybody that uses this.

Doxygen has quite a few rules to comply with (e.g. "To document a member of a C++ class, you must also document the class itself."), and having the full validation to ensure a compliant conversion in CXX may be an overkill and passing this responsibility to its users instead is IMHO ok.
But I understand that "feature" shouldn't be called "doxygen" in order not to set wrong expectations. Perhaps what could work is to have an optional `cxx_doc` attribute which could be used to control how comments look like in the generated files, e.g.:
```rust
#[cxx::bridge(namespace = "org::blobstore")]
mod ffi {
    /// Shared structs with fields visible to both languages.
    #[cxx_doc(comment_line = "//!")
    struct BlobMetadata {
...
```
or one could perhaps also use that to have a different comment content:
```rust
#[cxx::bridge(namespace = "org::blobstore")]
mod ffi {
    /// Shared structs with fields visible to both languages.
    #[cxx_doc(comment_line = "///", text = " Shared structs with fields\n visible to both languages.")
    struct BlobMetadata {
...
```

@alexeyr @gabrik what do you think?
I don't find that an appealing approach.

Regarding the _"to document a member of a C++ class, you must also document the class itself"_ rule, I agree we don't need cxx to check that.
@dtolnay what is your preference? Keep it outside CXX and leave it to custom post-processing scripts to be applied on generated files?

Fix patch:
diff --git a/gen/build/Cargo.toml b/gen/build/Cargo.toml
index 2519e179c..4615064ea 100644
--- a/gen/build/Cargo.toml
+++ b/gen/build/Cargo.toml
@@ -27,6 +27,7 @@ scratch = "1.0"
 syn = { version = "1.0.95", default-features = false, features = ["parsing", "printing", "clone-impls", "full"] }
 
 [dev-dependencies]
+cxx = { version = "1.0", path = "../.." }
 cxx-gen = { version = "0.7", path = "../lib" }
 pkg-config = "0.3"
 
diff --git a/gen/build/src/cfg.rs b/gen/build/src/cfg.rs
index d2d859cb6..69eb6945d 100644
--- a/gen/build/src/cfg.rs
+++ b/gen/build/src/cfg.rs
@@ -12,6 +12,8 @@ pub struct Cfg<'a> {
     pub exported_header_prefixes: Vec<&'a str>,
     /// See [`CFG.exported_header_links`][CFG#cfgexported_header_links].
     pub exported_header_links: Vec<&'a str>,
+    /// See [`CFG.doxygen`][CFG#cfgdoxygen].
+    pub doxygen: bool,
     marker: PhantomData<*const ()>, // !Send + !Sync
 }
 
@@ -232,12 +234,84 @@ pub struct Cfg<'a> {
 ///     cxx_build::bridge("src/bridge.rs").compile("demo");
 /// }
 /// ```
+///
+/// <p style="margin:0"><br><br></p>
+///
+/// <div style="float:right;margin:22px 50px 0;font-size:1.15em;opacity:.73"><strong>bool</strong></div>
+///
+/// ## **`CFG.doxygen`**
+///
+/// Boolean. Whether to propagate Rust documentation from inside the cxx::bridge
+/// module as Doxygen-style comments in the generated C++ header.
+///
+/// Documentation on the following are supported:
+///
+/// - shared structs, and fields of shared structs
+/// - shared enums, and their variants
+/// - extern "Rust" opaque types
+/// - extern "Rust" functions, including methods/member functions
+///
+/// ### Example
+///
+/// ```no_run
+/// // build.rs
+///
+/// use cxx_build::CFG;
+///
+/// fn main() {
+///     CFG.doxygen = true;
+///
+///     cxx_build::bridge("src/bridge.rs").compile("demo");
+/// }
+/// ```
+///
+/// ```rust
+/// // src/bridge.rs
+///
+/// #[cxx::bridge]
+/// mod ffi {
+///     /// documentation of MyStruct
+///     pub struct MyStruct {
+///         /// documentation of the struct field
+///         lol: String,
+///     }
+///
+///     extern "Rust" {
+///         /// documentation of MyType
+///         type MyType;
+///
+///         /// function documentation
+///         fn asdf() -> bool;
+///     }
+/// }
+/// #
+/// # pub struct MyType;
+/// # fn asdf() -> bool { true }
+/// # fn main() {}
+/// ```
+///
+/// With `CFG.doxygen` enabled, the generated C++ header through which
+/// downstream C++ code will be able to access these shared structs and extern
+/// "Rust" signatures will have the Rust documentation comments propagated as
+/// Doxygen-style comments:
+///
+/// ```cpp
+/// /// documentation of MyStruct
+/// struct MyStruct final {
+///   /// documentation of the struct field
+///   ::rust::String lol;
+///   …
+/// };
+/// ```
+///
+/// Otherwise by default (without `CFG.doxygen`) they'll just be `//` comments.
 #[cfg(doc)]
 pub static mut CFG: Cfg = Cfg {
     include_prefix: "",
     exported_header_dirs: Vec::new(),
     exported_header_prefixes: Vec::new(),
     exported_header_links: Vec::new(),
+    doxygen: false,
     marker: PhantomData,
 };
 
@@ -248,6 +322,7 @@ impl<'a> Debug for Cfg<'a> {
             exported_header_dirs,
             exported_header_prefixes,
             exported_header_links,
+            doxygen,
             marker: _,
         } = self;
         formatter
@@ -256,6 +331,7 @@ impl<'a> Debug for Cfg<'a> {
             .field("exported_header_dirs", exported_header_dirs)
             .field("exported_header_prefixes", exported_header_prefixes)
             .field("exported_header_links", exported_header_links)
+            .field("doxygen", doxygen)
             .finish()
     }
 }
@@ -280,6 +356,7 @@ mod r#impl {
         exported_header_dirs: Vec<InternedString>,
         exported_header_prefixes: Vec<InternedString>,
         exported_header_links: Vec<InternedString>,
+        doxygen: bool,
     }
 
     impl CurrentCfg {
@@ -290,11 +367,13 @@ mod r#impl {
             let exported_header_dirs = Vec::new();
             let exported_header_prefixes = Vec::new();
             let exported_header_links = Vec::new();
+            let doxygen = false;
             CurrentCfg {
                 include_prefix,
                 exported_header_dirs,
                 exported_header_prefixes,
                 exported_header_links,
+                doxygen,
             }
         }
     }
@@ -327,11 +406,13 @@ mod r#impl {
             let exported_header_dirs = current.exported_header_dirs.vec();
             let exported_header_prefixes = current.exported_header_prefixes.vec();
             let exported_header_links = current.exported_header_links.vec();
+            let doxygen = current.doxygen;
             super::Cfg {
                 include_prefix,
                 exported_header_dirs,
                 exported_header_prefixes,
                 exported_header_links,
+                doxygen,
                 marker: PhantomData,
             }
         }
@@ -397,6 +478,7 @@ mod r#impl {
                     exported_header_dirs,
                     exported_header_prefixes,
                     exported_header_links,
+                    doxygen,
                     marker: _,
                 } = cfg;
                 let mut current = CURRENT.write().unwrap_or_else(PoisonError::into_inner);
@@ -404,6 +486,7 @@ mod r#impl {
                 current.exported_header_dirs = vec::intern(exported_header_dirs);
                 current.exported_header_prefixes = vec::intern(exported_header_prefixes);
                 current.exported_header_links = vec::intern(exported_header_links);
+                current.doxygen = *doxygen;
             } else {
                 CONST_DEREFS.with(|derefs| derefs.borrow_mut().remove(&self.handle()));
             }
diff --git a/gen/build/src/lib.rs b/gen/build/src/lib.rs
index 7272a396c..225042c29 100644
--- a/gen/build/src/lib.rs
+++ b/gen/build/src/lib.rs
@@ -399,6 +399,7 @@ fn generate_bridge(prj: &Project, build: &mut Build, rust_source_file: &Path) ->
     let opt = Opt {
         allow_dot_includes: false,
         cfg_evaluator: Box::new(CargoEnvCfgEvaluator),
+        doxygen: CFG.doxygen,
         ..Opt::default()
     };
     let generated = gen::generate_from_path(rust_source_file, &opt);
diff --git a/gen/src/mod.rs b/gen/src/mod.rs
index 351ac187e..f24846a7e 100644
--- a/gen/src/mod.rs
+++ b/gen/src/mod.rs
@@ -59,6 +59,7 @@ pub struct Opt {
     pub(super) gen_implementation: bool,
     pub(super) allow_dot_includes: bool,
     pub(super) cfg_evaluator: Box<dyn CfgEvaluator>,
+    pub(super) doxygen: bool,
 }
 
 pub(super) trait CfgEvaluator {
@@ -89,6 +90,7 @@ impl Default for Opt {
             gen_implementation: true,
             allow_dot_includes: true,
             cfg_evaluator: Box::new(UnsupportedCfgEvaluator),
+            doxygen: false,
         }
     }
 }
diff --git a/gen/src/write.rs b/gen/src/write.rs
index fa8ffe0bd..ad55fe4ec 100644
--- a/gen/src/write.rs
+++ b/gen/src/write.rs
@@ -228,8 +228,21 @@ fn pick_includes_and_builtins(out: &mut OutFile, apis: &[Api]) {
 }
 
 fn write_doc(out: &mut OutFile, indent: &str, doc: &Doc) {
+    let mut lines = 0;
     for line in doc.to_string().lines() {
-        writeln!(out, "{}//{}", indent, line);
+        if out.opt.doxygen {
+            writeln!(out, "{}///{}", indent, line);
+        } else {
+            writeln!(out, "{}//{}", indent, line);
+        }
+        lines += 1;
+    }
+    // According to https://www.doxygen.nl/manual/docblocks.html, Doxygen only
+    // interprets `///` as a Doxygen comment block if there are at least 2 of
+    // them. In Rust, a single `///` is definitely still documentation so we
+    // make sure to propagate that as a Doxygen comment.
+    if out.opt.doxygen && lines == 1 {
+        writeln!(out, "{}///", indent);
     }
 }
 
diff --git a/third-party/Cargo.lock b/third-party/Cargo.lock
index 38b0a01c7..11608fae4 100644
--- a/third-party/Cargo.lock
+++ b/third-party/Cargo.lock
@@ -98,6 +98,7 @@ version = "1.0.81"
 dependencies = [
  "cc",
  "codespan-reporting",
+ "cxx",
  "cxx-gen",
  "once_cell",
  "pkg-config",


Test patch: