Skip to content

Auto register metafunctions (POC) #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 82 commits into
base: program-defined_metafunctions_v2
Choose a base branch
from

Conversation

DyXel
Copy link

@DyXel DyXel commented Feb 20, 2024

This is an proof-of-concept implementation (works, though I won't call it mergeable) to illustrate the idea I was having around in regards to auto-registration of metafunctions and possibly adopt it for the main PR. It also includes the patch to remove CPPFRONT_METAFUNCTION_LIBRARY usage by @/MaxSagebaum (thanks btw!). In summary, it does the following:

  • Remove global mangled names generation
  • Remove the per-file entry-point generation
  • Instead, generate a static global object called cpp2::meta::register_function for each metafunction, for which the fully-qualified name is passed, as well as a pointer to the metafunction as arguments to the constructor. (this works similarly to how register_flag in cppfront works today)
  • The implementation of cpp2::meta::register_function is then provided separately, and its mean to be used when building/linking the final library, along a unambiguous extern "C" function which becomes the only entry-point of the DLL (all in meta_lib_impl.cpp)

Usage then becomes (writing down example with multiple TUs/files to better show how this scales):

metafunctions.cpp2:

greeter: (inout t: cpp2::meta::type_declaration) = {
  t.add_member($R"(say_hi: () = std::cout << "Hello, world!\nFrom (t.name())$\n";)");
}

metafunctions2.cpp2:

greeter2: (inout t: cpp2::meta::type_declaration) = {
  t.add_member($R"(say_hi: () = std::cout << "Hello, world! 2\nFrom (t.name())$\n";)");
}

main.cpp2:

my_class: @greeter type   = { }
my_class2: @greeter2 type = { }
main: ()                  = {
	my_class().say_hi();
	my_class2().say_hi();
}

Build cppfront:

g++ -rdynamic -std=c++20 cppfront.cpp -o cppfront

Build metafunctions lib:

./cppfront metafunctions.cpp2 metafunctions2.cpp2
g++ -std=c++20 -fPIC -shared -o libmetafunctions.so metafunctions.cpp metafunctions2.cpp meta_lib_impl.cpp

Build and run main:

CPPFRONT_METAFUNCTION_LIBRARIES=./libmetafunctions.so ./cppfront main.cpp2
g++ -std=c++20 main.cpp -o main
./main

Output:

metafunctions.cpp2... ok (all Cpp2, passes safety checks)

metafunctions2.cpp2... ok (all Cpp2, passes safety checks)

main.cpp2... ok (all Cpp2, passes safety checks)

Hello, world!
From my_class
Hello, world! 2
From my_class2

Generated Cpp1 code for completeness:
metafunctions.cpp:

//=== Cpp2 type declarations ====================================================


#include "cpp2util.h"

#line 1 "metafunctions.cpp2"


//=== Cpp2 type definitions and function declarations ===========================

#line 1 "metafunctions.cpp2"
auto greeter(cpp2::meta::type_declaration& t) -> void;

//=== Cpp2 function definitions =================================================

#line 1 "metafunctions.cpp2"
auto greeter(cpp2::meta::type_declaration& t) -> void{
#line 2 "metafunctions.cpp2"
  CPP2_UFCS(add_member)(t, R"(say_hi: () = std::cout << "Hello, world!\nFrom )" + cpp2::to_string(CPP2_UFCS(name)(t)) + R"(\n";)");
}

namespace { // Metafunctions registry
cpp2::meta::register_function rf1("::greeter",std::addressof(::greeter));
}

metafunctions2.cpp:

//=== Cpp2 type declarations ====================================================


#include "cpp2util.h"

#line 1 "metafunctions2.cpp2"


//=== Cpp2 type definitions and function declarations ===========================

#line 1 "metafunctions2.cpp2"
auto greeter2(cpp2::meta::type_declaration& t) -> void;

//=== Cpp2 function definitions =================================================

#line 1 "metafunctions2.cpp2"
auto greeter2(cpp2::meta::type_declaration& t) -> void{
#line 2 "metafunctions2.cpp2"
  CPP2_UFCS(add_member)(t, R"(say_hi: () = std::cout << "Hello, world! 2\nFrom )" + cpp2::to_string(CPP2_UFCS(name)(t)) + R"(\n";)");
}

namespace { // Metafunctions registry
cpp2::meta::register_function rf1("::greeter2",std::addressof(::greeter2));
}

main.cpp:

//=== Cpp2 type declarations ====================================================


#include "cpp2util.h"

#line 1 "main.cpp2"
class my_class;
#line 2 "main.cpp2"
class my_class2;


//=== Cpp2 type definitions and function declarations ===========================

#line 1 "main.cpp2"
class my_class             {
public: static auto say_hi() -> void;

      public: my_class() = default;
      public: my_class(my_class const&) = delete; /* No 'that' constructor, suppress copy */
      public: auto operator=(my_class const&) -> void = delete;
  };
#line 2 "main.cpp2"
class my_class2 {
public: static auto say_hi() -> void;

      public: my_class2() = default;
      public: my_class2(my_class2 const&) = delete; /* No 'that' constructor, suppress copy */
      public: auto operator=(my_class2 const&) -> void = delete;
};
#line 3 "main.cpp2"
auto main() -> int;

//=== Cpp2 function definitions =================================================

#line 1 "main.cpp2"


auto my_class::say_hi() -> void { std::cout << "Hello, world!\nFrom my_class\n"; }
auto my_class2::say_hi() -> void { std::cout << "Hello, world! 2\nFrom my_class2\n"; }
#line 3 "main.cpp2"
auto main() -> int        {
 CPP2_UFCS(say_hi)(my_class());
 CPP2_UFCS(say_hi)(my_class2());
}

Follow up

@MaxSagebaum you mentioned here hsutter#907 (comment) that you'd prefer Option 1, over 2 (what I did here), could you elaborate on why?

After drafting this code, I feel like with this approach, most of the code currently in the main PR becomes a bit redundant:

  • No need for mangling implementations at all
  • No need to look up multiple symbols, just an exact name is enough, which makes dll_symbol unnecessary
  • No extra code generation besides the registry bit at the very end
  • Simpler usage for the end-user, all they need to do is provide meta_lib_impl.cpp. It also acts as a customization point if needed.

What do you think? Would this be a direct improvement? I am not sure which caveats it might have over the existing solution, lets discuss here!

JohelEGP and others added 30 commits December 24, 2023 21:36
A metafunction is normal Cpp2 code compiled as part of a library.
When parsing a declaration that `@`-uses the metafunction,
the library is loaded and the metafunction invoked on the declaration.

The reflection API is available by default to Cpp2 code (via `cpp2util.h`).
The implementation of the API is provided by the `cppfront` executable.
For this to work, compiling `cppfront` should export its symbols
(for an explanation, see <https://cmake.org/cmake/help/latest/prop_tgt/ENABLE_EXPORTS.html>).

The default build of `cppfront` doesn't support loading program-defined metafunctions.
In order to support loading program-defined metafunctions,
`cppfront` should be built with `CPPFRONT_LOAD_METAFUNCTION_IMPL_HEADER`
defined to a header which implements the loading functionality.
This commit includes an implementation using [Boost.DLL][].
[Boost.DLL]: https://www.boost.org/doc/libs/release/doc/html/boost_dll.html

Because cppfront doesn't perform name lookup,
metafunction names should be `@`-used unqualified
and follow C "namespacing" conventions (e.g., `@mylib_mymetafunction`).

Here is an example of program-defined metafunctions using the Boost.DLL implementation.
The commands were cleaned up from the CMake buildsystem in hsutter#797.

`metafunctions.cpp2`:
```Cpp2
greeter: (inout t: cpp2::meta::type_declaration) = {
  t.add_member($R"(say_hi: () = std::cout << "Hello, world!\nFrom (t.name())$\n";)");
}
```

`main.cpp2`:
```Cpp2
my_class: @greeter type = { }
main: ()                = my_class().say_hi();
```

Build `cppfront`:
```bash
g++ -DBOOST_ATOMIC_DYN_LINK -DBOOST_ATOMIC_NO_LIB -DBOOST_FILESYSTEM_DYN_LINK -DBOOST_FILESYSTEM_NO_LIB -DBOOST_SYSTEM_DYN_LINK -DBOOST_SYSTEM_NO_LIB -DCPPFRONT_LOAD_METAFUNCTION_IMPL_HEADER=\"reflect_load_metafunction_boost_dll.h\" -std=c++20 -o cppfront.cpp.o -c cppfront.cpp
g++ -Wl,--export-dynamic -rdynamic cppfront.cpp.o -o cppfront /usr/lib/libboost_system.so.1.83.0  /usr/lib/libboost_filesystem.so.1.83.0  /usr/lib/libboost_atomic.so.1.83.0
```

Build `metafunctions`:
```bash
./cppfront metafunctions.cpp2
g++ -std=c++20 -fPIC -o metafunctions.cpp.o -c metafunctions.cpp
g++ -fPIC -shared -Wl,-soname,libmetafunctions.so -o libmetafunctions.so metafunctions.cpp.o
```

Build and run `main`:
```bash
CPPFRONT_METAFUNCTION_LIBRARIES=libmetafunctions.so ./cppfront main.cpp2
g++ -std=c++20 -o main.cpp.o -c main.cpp
g++ main.cpp.o -o main
./main
```

Output:
```output
metafunctions.cpp2... ok (all Cpp2, passes safety checks)

main.cpp2... ok (all Cpp2, passes safety checks)

Hello, world!
From my_class
```
Properly take a std::string in those functions to ensure that the passed strings are actually properly null terminated.
Check the ``_WIN32`` macro rather than ``_MSC_VER`` to detect if we're targeting Windows, rather than checking if we're compiling using Visual Studio.
Add ``function_cast`` to properly convert the function pointer retrieved from GetProcAddress when building under mingw.
CPPFRONTAPI should be used to expose c++ functions that would be used by
metafunctions. The question remains how to mark cpp2 code with this during
lowering, at the moment manually marking the generated files with it makes
Windows work.

CPP2_C_API is emitted when generating the "entrypoint" for each metafunction by
making it extern "C".
At some point those strings will need to be reported anyways, and its unbearable
to test without zero error logging.
…st-dll

refactor(reflect): remove Boost.DLL dependency by doing DLL handling ourselves
Regenerate `reflect.h2` with the following commands.
```
cppfront -p reflect.h2 -o cpp2reflect.h
mv cpp2reflect.h ../include/
```
JohelEGP and others added 23 commits January 1, 2024 21:19
@MaxSagebaum
Copy link

I tested your implementation and it works. But meta_lib_impl.cpp is not really necessary. I modified you branch, such that meta_lib_impl.cpp could be moved to reflect_impl.h2. This would make the implementation even simpler.

simplification.patch.txt

@MaxSagebaum you mentioned here hsutter#907 (comment) that you'd prefer Option 1, over 2 (what I did here), could you elaborate on why?

The loading order of static symbols is not defined in Cpp1 and I had problems with this in the past. I thought about it a little bit more and for this kind of application it should not matter. So option 2 should be fine.

@DyXel
Copy link
Author

DyXel commented Feb 21, 2024

I modified you branch, such that meta_lib_impl.cpp could be moved to reflect_impl.h2. This would make the implementation even simpler.

Oh, I thought about that too, but my concern with it is that it introduces extra global state to cppfront, in order to keep track where symbols belong per library (constraint 3 point c in the constraints post), as you'd need to setup global context that the register_function constructor within cppfront can use, this might or might not matter in the end, but good to keep in mind.

The loading order of static symbols is not defined in Cpp1 and I had problems with this in the past. I thought about it a little bit more and for this kind of application it should not matter. So option 2 should be fine.

Yeah, I too know how problematic order of static constructors can be, but here we defer all of that to cppfront, it can decide how exactly to build up the look-up tree from the registry, and even detect duplicate entries (so we can deal with subtle ODR violations).

@MaxSagebaum
Copy link

Oh, I thought about that too, but my concern with it is that it introduces extra global state to cppfront, in order to keep track where symbols belong per library (constraint 3 point c in hsutter#907 (comment)), as you'd need to setup global context that the register_function constructor within cppfront can use, this might or might not matter in the end, but good to keep in mind.

With your current implementation, every library needs to setup a local state.

The global state in cppfront is not really required. The register_functions could just call a function handle that is set by cppfront prior to the loading of the dll.

I do not really see the problem with constraint 3 point c. You know which dll you are loading and can use this information to tag each meta function. If a second metafunction with the same name is loaded, then you can throw a nice error. This would be nearly the same as it currently is.

@DyXel
Copy link
Author

DyXel commented Feb 21, 2024

The register_functions could just call a function handle that is set by cppfront prior to the loading of the dll.

I do not really see the problem with constraint 3 point c. You know which dll you are loading and can use this information to tag each meta function.

yes, the "set by cppfront prior to loading the dll" part is what I mean by "global state", you'd need a global variable to setup that function handle, which needs to be seen by register_function ctor, then you can load the dll and continue forward. It is minuscule, but still there. Whereas if the register_function implementation is inside the dll, all you need to do is call the exported function, and directly process the array of items returned, without anything additional in cppfront side. Like I said, its maybe not a big deal, and probably worth putting that extra complexity on cppfront side is better for UX.

@JohelEGP JohelEGP force-pushed the program-defined_metafunctions_v2 branch 2 times, most recently from 6f2c71b to 613f352 Compare October 9, 2024 20:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants