Skip to content

optional any generates uncompilable C++ and Rust code #3805

Description

@BaldDemian

Search before asking

  • I had searched in the issues and found no similar issues.

Version

Latest Fory

Component(s)

Rust, C++

Minimal reproduce step

Generate C++ and Rust code using this IDL:

package issue.optional_any;

message Holder {
  optional any value = 1;
}

and compile the generated code with g++ and cargo.

Note: since Fory C++ compiler only generate header files for IDL, we should define and compile the simple source file like this:

#include "issue_optional_any.h"

int main() {
  issue::optional_any::Holder holder;
  return holder.has_value() ? 1 : 0;
}

What did you expect to see?

Code should be compiled.

What did you see instead?

Compilation failed for both C++ and Rust.

g++ compilation error:

In file included from /tmp/fory-cpp-optional-any.3A0fEE/compile.cc:1:
/tmp/fory-cpp-optional-any.3A0fEE/issue_optional_any.h: In member function ‘const std::any& issue::optional_any::Holder::value() const’:
/tmp/fory-cpp-optional-any.3A0fEE/issue_optional_any.h:52:12: error: no match for ‘operator*’ (operand type is ‘const std::any’)
   52 |     return *value_;
      |            ^~~~~~~

cargo compilation error:

error[E0277]: the trait bound `dyn Any + Send + Sync: Default` is not satisfied
    --> src/issue_optional_any.rs:24:5
     |
  21 | #[derive(::fory::ForyStruct, Default)]
     |                              ------- in this derive macro expansion
...
  24 |     pub value: ::std::sync::Arc<dyn ::std::any::Any + Send + Sync>,
     |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Default` is not implemented for `dyn Any + Send + Sync`
     |
help: the following other types implement trait `Default`
    --> /home/hpy/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/sync.rs:3715:1
     |
3715 | impl<T: Default> Default for Arc<T> {
     | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `Arc<T>`
...
3767 | impl Default for Arc<str> {
     | ^^^^^^^^^^^^^^^^^^^^^^^^^ `Arc<str>`
...
3782 | impl Default for Arc<core::ffi::CStr> {
     | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `Arc<CStr>`
...
3801 | impl<T> Default for Arc<[T]> {
     | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `Arc<[T]>`
     = note: required for `Arc<dyn Any + Send + Sync>` to implement `Default`

error[E0599]: no method named `is_some` found for struct `Arc<(dyn Any + Send + Sync + 'static)>` in the current scope
  --> src/issue_optional_any.rs:31:23
   |
31 |         if self.value.is_some() {
   |                       ^^^^^^^ method not found in `Arc<(dyn Any + Send + Sync + 'static)>`

Some errors have detailed explanations: E0277, E0599.
For more information about an error, try `rustc --explain E0277`.
error: could not compile `optional_any_repro` (lib) due to 2 previous errors

Anything Else?

Root cause

Let's analyze the generated code.

The generated C++ code:

class Holder final {
  public:

  bool has_value() const {
    return value_.has_value();
  }

  const std::any& value() const {
    return *value_;
  }

  void set_value(std::any value) {
    value_ = std::move(value);
  }

  void clear_value() {
    value_.reset();
  }

  bool operator==(const Holder& other) const {
    return ((!value_.has_value() && !other.value_.has_value()) || (value_.type() == other.value_.type()));
  }

  private:
    std::any value_;

  public:
  FORY_STRUCT(Holder, (value_, fory::F(1).nullable()));
};

We can see Fory generates C++ std::any for IDL optional any. This is correct behavior as defined in the doc:

- `any` always writes a null flag (same as `nullable`) because values may be empty.

But the real issue here is that generate_field_accessors() chooses the optional-field accessor path from field.optional, and emits

return *value_;

if field.optional:
lines.append(f"{indent} return *{member_name};")

This expression is valid for std::optional<T>, but invalid for std::any since it is not dereferenceable.

And the generated Rust code:

#[derive(::fory::ForyStruct, Default)]
pub struct Holder {
    #[fory(id = 1, nullable = true)]
    pub value: ::std::sync::Arc<dyn ::std::any::Any + Send + Sync>,
}

impl ::std::fmt::Debug for Holder {
    fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
        f.write_str("Holder { ")?;
        f.write_str("value: ")?;
        if self.value.is_some() {
            f.write_str("any(...)")?;
        } else {
            f.write_str("None")?;
        }
        f.write_str(" }")
    }
}

In Rust, optional any still generates a bare Arc<dyn Any + Send + Sync> field instead of an Option<...>, which is the same as C++.

But other Rust generation paths still look only at field.optional:

Option has the is_some() method and implements Debug, but not for Arc<dyn Any + Send + Sync>.

Suggested fix

At first I though of fixing each edge cases in both Rust and C++ compiler case by case.
But I didn't know whether optional any would trigger edge cases in other languages.

The doc already says any as writing a null flag, same as a
nullable field. Therefore, optional any appears to be redundant with plain
any.

So instead of adding language-specific edge-case handling for every generated path,
why won't we canonicalize optional any directly into any in the IR?

e.g.

optional any value = 1;

should be normalized to:

any value = 1;

This would fix above edge cases directly.

Are you willing to submit a PR?

  • I'm willing to submit a PR!

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions