Skip to content

Conversation

@no-more-secrets
Copy link
Contributor

@no-more-secrets no-more-secrets commented Dec 10, 2020

This PR adds compile-time static reflection facilities to Flatbuffers' Tables and Structs in C++17 mode. Specifically:

  1. The C++ generator has been modified so that, when in C++17 mode, it will emit Table and Struct field traits that can be used at both compile time and runtime jointly as a form of static reflection. This includes field types, field names, and a generic field-getter approach based on a tuple.

  2. A comprehensive unit test that also serves as an example. It demonstrates the power of this reflection by implementing a full recursive stringifier for arbitrary Flatbuffers types, but without needing any runtime access to the *.fbs definition files; the computation is done using only the static reflection facilities introduced in this commit and is fully generic.

  3. Built in C++17 mode and ran generate_code.sh, and included diffs in this commit. Only cpp17 generated files are affected.

Tested on Linux with gcc 10.2.0, but should work on all compilers.

Fixes #6285

Included in this commit is the following:

  - The C++ generator has been modified so that,
    when in C++17 mode, it will emit Table and
    Struct field traits that can be used at com-
    pile time as a form of static reflection. This
    includes field types, field names, and a tuple
    of field getter results.

  - Diffs to the cpp17 generated files. No other
    generated files are affected.

  - A unit test that also serves as an example. It
    demonstrates how to use the full power of this
    reflection to implement a full recursive
    JSON-like stringifier for Flatbuffers types,
    but without needing any runtime access to the
    *.fbs definition files; the computation is
    done using only static reflection.

Tested on Linux with gcc 10.2.0.

Fixes google#6285.
@no-more-secrets
Copy link
Contributor Author

@aardappel @vglavnyy Please take a look; this should be exactly what we talked about in #6285.

The one thing that I wasn't sure about was the concern that you both raised that it "would be good to ensure fields_pack never constructs anything"; the fields_pack as I have implemented it just returns a std::tuple of light-weight things, such as pointers, references, ints, etc., and thus should not construct any objects or cause any performance concerns. But let me know if there is something that I'm missing.

Thanks in advance for your consideration :)

@no-more-secrets
Copy link
Contributor Author

no-more-secrets commented Dec 10, 2020

As you might be aware, there are a bunch of diffs to generated python files that are unrelated to my change... perhaps someone forgot to commit them recently? As far as I can see, all of the CI checks that are failing are doing so only for that reason. See PR #6325.

Copy link
Collaborator

@aardappel aardappel left a comment

Choose a reason for hiding this comment

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

Generally looks very good to me!

* limitations under the License.
*/

// This contains some utilities/examples for how to leverage the static reflec-
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is great for testing.. I wonder if some of it can be generic enough that its worth sticking in include/flatbuffers ? Wonder what happens if someone actually wants to use this functionality, right now they'd probably to copy this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes this is a standalone utility that someone could include and use in their own project if they are compiling with C++17. If you like I can move it into the include/flatbuffers folder so that they can include it more easily.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@vglavnyy do you think that makes sense?

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't mind. However, we already have a lot of changes in C++ code: optional, span, --cpp-std c++11.
Maybe move it into the include folder after release?

Copy link
Contributor Author

@no-more-secrets no-more-secrets Dec 13, 2020

Choose a reason for hiding this comment

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

Ok for now I will just leave it in the test folder, and we can move it at a later time.

Another thing to think about -- it is possible to parse this string and produce a real object with roughly the same amount of code/methodology. Perhaps we might want to include that as well when the time comes.

Another use case of this reflection is that we can provide a generic function called FlatbuffersEquals that will compare two Flatbuffers of the same type and tell you if they are equal or not. It will do this by iterating through the fields and comparing them, again using the static reflection. I believe traditionally this has not been possible to compare two buffers right? Or can you just do a memcmp() on the binary data? IIUC that would only work if the fields were added in the same order when creating the buffers?

}

const Monster* BuildMonster(flatbuffers::FlatBufferBuilder& fbb) {
using ::cpp17::MyGame::Example::MonsterBuilder;
Copy link
Collaborator

Choose a reason for hiding this comment

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

why do we need another function creating a buffer? don't we have existing buffers we can test?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wanted to use a Monster object for this test because it has many fields of all different types (including nested structs) in order to exercise all of the possibilities. However, in this particular test file (test_cpp27.cpp) there is not an existing function that creates a Monster object like this, so I have added this one. If there is another one somewhere else that you are aware of that I can reuse (which has all the different field types represented and populated) let me know and I am happy to reuse it.

Copy link
Contributor

Choose a reason for hiding this comment

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

CreateMonsterDirect + GetBuffer (or GetBufferSpan) + flatbuffers::GetRoot<Monster>?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I started changing to this approach (CreateMonsterDirect) but that seems a bit inconvenient in this case because it will force me to put many parameters corresponding to fields that I am not using, since I am populating a few fields towards the end, I cannot rely on the default values and I would have many parameters just nullptr or 0. Perhaps in this case where we are just populating a few select fields the Builder approach is better?

@github-actions github-actions bot added c++ codegen Involving generating code from schema labels Dec 11, 2020
@vglavnyy
Copy link
Contributor

vglavnyy commented Dec 13, 2020

@dpacbach
It doesn't compile with MSVC2019.
Commit 2046bff removed MSVC2017 and MSVC2019 from Appveryor-CI, so it passed CI but in fact, it doesn't work.
The Github action CI / Build Windows build uses MSVC2019 but it doesn't set FLATBUFFERS_BUILD_CPP17 option:

Run cmake -G "Visual Studio 16 2019" -A x64 -DCMAKE_BUILD_TYPE=Release .

Need to adjust https://github.com/google/flatbuffers/blob/master/.github/workflows/build.yml file.
Or adjust CMakeLists.txt to make FLATBUFFERS_BUILD_CPP17 option active if a modern compiler is used.

Update:
This is a possible fix for MSVC2019.
It fixes all warnings. Also StringifyVector reimplemented without using std::ostringstream.
This is only one of the possible options, I do not insist on it.

I've checked this code without Treat Warnings as Errors ('/WX`). This test failed, call stack added to commit message.

}

const Monster* BuildMonster(flatbuffers::FlatBufferBuilder& fbb) {
using ::cpp17::MyGame::Example::MonsterBuilder;
Copy link
Contributor

Choose a reason for hiding this comment

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

CreateMonsterDirect + GetBuffer (or GetBufferSpan) + flatbuffers::GetRoot<Monster>?

@no-more-secrets
Copy link
Contributor Author

@vglavnyy Thanks for helping me to fix the MSVC issues, I will make those changes that you suggest in your commit. But I am not clear on something that you said:

I've checked this code without Treat Warnings as Errors ('/WX`). This test failed, call stack added to commit message.

I did not see the call stack in your commit, how do I view it? But actually, I think there is a minor bug in your code at the end of the StringifyVector that would (at least) account for the test failure:

  text += "]";
  return text;
}

I believe that should be text += std::string("\n") + indent + "]"; right? Otherwise, the ] will be touching the final element in the vector. In any case, I will make your changes and test the fix and then we'll try again with MSVC.

Regarding the MSVC 19 C++17 build in the CI... do you want me to enable C++17 in that build.yml file in this PR, or will you just do the testing for me? Or do you want to enable it but in a separate PR? Thank you

@vglavnyy
Copy link
Contributor

Comit 7575a2c (without changes)

flattests_cpp17.exe!flatbuffers::FlatBufferBuilder::NotNested() Line 1415 C++
flattests_cpp17.exe!flatbuffers::FlatBufferBuilder::CreateString(const char * str, unsigned __int64 len) Line 1535 C++
flattests_cpp17.exe!flatbuffers::FlatBufferBuilder::CreateString(const char * str) Line 1546 C++
flattests_cpp17.exe!BuildMonster(flatbuffers::FlatBufferBuilder & fbb) Line 70 C++
flattests_cpp17.exe!StringifyAnyFlatbuffersTypeTest() Line 109 C++
flattests_cpp17.exe!FlatBufferCpp17Tests() Line 242 C++
flattests_cpp17.exe!main(int __formal, const char * * __formal) Line 250 C++

Probably no one CI execute cpp17 tests.

Regarding the MSVC 19 C++17 build in the CI... do you want me to enable C++17 in that build.yml file in this PR, or will you just do the testing for me? Or do you want to enable it but in a separate PR? Thank you

For this PR fix of build.yml for Build Windows target will be sufficient to see that this code works well.

The current status of the c++17 code generator is experimental. But it would be better to enable compilation of flattests_cpp17 on all compatible CI to get more information from different compilers.
I will increase compilation time by about 30-60 seconds for each platform but I think it is acceptable.
The easiest way to do this - adjust CMakeListst.txt. This can be done late in a separate PR.

@github-actions github-actions bot added the CI Continuous Integration label Dec 13, 2020
@no-more-secrets
Copy link
Contributor Author

@vglavnyy update:

  • I enabled the C++17 build in MSVC 19 first and reproduced the warnings/failures. Note that I have included those build.yml changes in this PR.
  • I made @vglavnyy 's fixes to eliminate the warnings on MSVC (although I also made another change that no longer requires suppressing the "unreachable code" warning).
  • I fixed the test failure on MSVC (it was the dreaded NotNested error).
  • Now the windows CI builds all pass.
  • Implemented all of @vglavnyy 's other suggestions with the exception of the CreateMonsterDirect.
  • I added a fully_qualified_name field to the code gen that gives the also the namespace, which I'm sure will be needed in some cases since the table/struct name only can be ambiguous. The unit test now uses that fully qualified name.
  • I ran clang-format on all of the files I touched.

@aardappel
Copy link
Collaborator

Ah, wasn't aware that moving VS2017/19 to GitHub Actions removed the C++17 testing, sorry!

@no-more-secrets
Copy link
Contributor Author

@aardappel We are ready for your review, PTAL when you have a chance. Just wanted to point out though that the broken CI check above appears to be coming from another change in master, I don't quite understand what it means. Apart from that, all the checks look good.

Copy link
Collaborator

@aardappel aardappel left a comment

Choose a reason for hiding this comment

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

This LGTM.
I wonder, since it generates a fair bit of code and it is somewhat niche in use, wether it should be behind a flag. After all, we have less niche uses of optional things behind a flag, e.g. --gen-object-api and --gen-mutable etc. It's not the biggest deal, but I know we have people relying on FlatBuffers being small in code size.


if (opts_.gen_nullable) { code_ += "#pragma clang system_header\n\n"; }

if (opts_.g_cpp_std >= cpp::CPP_STD_17) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

should this instead go into base.h or similar?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think I'd probably avoid that, because the <tuple> header is a bit long and template heavy, and so we probably don't want to force users to include it unless they have this C++ static reflection feature enabled (with C++17).

@no-more-secrets
Copy link
Contributor Author

@aardappel I'm fine to put it behind a flag, but I will let you guys decide since it won't affect me either way (I will always have that flag on). If you decide that you want a flag, let me know what you want to call it (I suggest perhaps --cpp-static-reflection). I guess that flag would then require that --cpp-std is at least c++17, and we'd have to add logic somewhere to enforce that.

@no-more-secrets
Copy link
Contributor Author

@vglavnyy what do you think?

One thing that I would point out is that the majority of the C++17 features that we've added thus far (and the ones that I have an idea to add in the future, such as static reflection for enums which is now lacking) have to do with static reflection. So if we put those behind a flag, then the --cpp-std=c++17 flag effectively won't do much at all on its own without also enabling the proposed static reflection flag. That said, I do understand the principle of it, since theoretically in the future someone might add some C++17 specific code-gen that is not to do with static reflection.

@vglavnyy
Copy link
Contributor

vglavnyy commented Mar 4, 2021

I think --cpp-static-reflection is the correct name for this feature.
This flag can set a opts_.cpp_static_reflection option if both --cpp-std=c++17 and --cpp-static-reflection are set.

@aardappel
Copy link
Collaborator

Yes, still think it would be good behind a flag. The use of C++17 is sure to expand in the future.

@no-more-secrets
Copy link
Contributor Author

no-more-secrets commented Mar 5, 2021

@aardappel @vglavnyy Ok I have added the flag --cpp-static-reflection, and I've put all of the C++17 stuff added thus far behind it, because it all happens to pertain to reflection (even the Create method from a past PR). Then in the idl_gen_cpp.cpp I have it check that if --cpp-static-reflection is enabled, then --cpp-std must be at least C++17 otherwise it fails with a flatc compile error. Also I have enabled --cpp-static-reflection by default in the generate_code.sh script otherwise the C++17 tests don't pass. PTAL

@no-more-secrets
Copy link
Contributor Author

@aardappel I have addressed all of @vglavnyy 's suggestions and he has approved, and the CI checks look good as far as I can see (I think there is one failure there that is not related to my change), so now just waiting for your final approval and we can merge.

@aardappel
Copy link
Collaborator

Looks good, thanks!

@aardappel aardappel merged commit a69815f into google:master Mar 5, 2021
@no-more-secrets no-more-secrets deleted the cpp17-field-traits branch March 5, 2021 21:49
@no-more-secrets no-more-secrets restored the cpp17-field-traits branch March 6, 2021 00:32
@no-more-secrets no-more-secrets deleted the cpp17-field-traits branch March 6, 2021 00:33
vglavnyy added a commit to vglavnyy/flatbuffers that referenced this pull request Mar 6, 2021
@vglavnyy
Copy link
Contributor

vglavnyy commented Mar 6, 2021

@dpacbach you forget to add --cpp-static-reflection to the generate_code.bat.
And probably flatc without --cpp-static-reflection doesn't generate Traits struct. I thought this flag should control only newly added code.

@no-more-secrets
Copy link
Contributor Author

@vglavnyy I sent PR #6502 to fix the flag. And yes I did put the entire Traits struct behind the flag, because I felt that everything it contains is for the purpose of some kind of reflection, even the generic Create method. Do you think we should change that?

@vglavnyy
Copy link
Contributor

vglavnyy commented Mar 6, 2021

This will break existing code that already depends on Create and type traits (I use Create).
Of course, it is possible to re-enable it again with --cpp-static-reflection but I don't feel that this the right way.
There are no reasons to break the working code.

@no-more-secrets
Copy link
Contributor Author

@vglavnyy Ok I will send a PR shortly to restore it back to the way it was.

aardappel pushed a commit that referenced this pull request Mar 11, 2021
* [idl_parser] Add kTokenNumericConstant token

This commit adds the new token for correct parsing of signed numeric constants.
Before this expressions `-nan` or `-inf` were treated as kTokenStringConstant.
This was ambiguous if a real string field parsed.
For example, `{ "text_field" : -name }` was accepted by the parser as valid JSON object.

Related oss-fuzz issue: 6200301176619008

* Add additional positive tests fo 'inf' and 'nan' as identifiers

* Rebase to HEAD

* Move processing of signed constants to ParseSingleValue method.

* Add missed `--cpp-static-reflection` (#6324) to pass CI

* Remove `flatbuffers.pc` from repository to unblock CI (#6455).

Probably the generated flatbuffers.pc should not be a part of repo.

* Fix FieldIdentifierTest()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++ CI Continuous Integration codegen Involving generating code from schema

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[C++17] [New Feature] Type-based Table Field Getters.

3 participants