Skip to content
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

Feature request: compile time constexpr in Rust via cpp!() #95

Open
ratijas opened this issue Jul 1, 2021 · 1 comment
Open

Feature request: compile time constexpr in Rust via cpp!() #95

ratijas opened this issue Jul 1, 2021 · 1 comment

Comments

@ratijas
Copy link
Contributor

ratijas commented Jul 1, 2021

Summary

I want to evaluate C++ code at compile time, and use produced values in const-expr contexts Rust which is a more strict requirement than just any rvalue.

cpp!(const i32 as "int" { /**/ })
// or if you prefer more syntax noise:
cpp!(const : i32 as "int" { /**/ })
cpp!(const -> i32 as "int" { /**/ })

This is going to be used in expressions rather than statements, thus return keyword and ; semicolon are not needed.

Description

Some things need to be known at compile time, such as const generics and enum values. Offloading them until run time is technically not an option. Thus, would be nice to have cpp!() macro compute C++ expressions while building and generating Rust code.

The first and primary use case for this are enums: copy-pasting values back and forth is an error-prone approach. Imagine writing this instead:

enum ApplicationAttribute
{
    // AA_ImmediateWidgetCreation = 0,
    // AA_MSWindowsUseDirect3DByDefault = 1,
    AA_DontShowIconsInMenus = 2,
    AA_NativeWindows = 3,
    AA_DontCreateNativeWidgetSiblings = 4,
    AA_PluginApplication = 5,
    AA_DontUseNativeMenuBar = 6,
    AA_MacDontSwapCtrlAndMeta = 7,
    ...
}
cpp!{{
    #include <QtCore/Qt>
}}

#[derive(Copy, Clone, Eq, PartialEq)]
#[repr(i32)]
enum ApplicationAttribute
{
    AA_DontShowIconsInMenus           = cpp!(const i32 as "int" { Qt::AA_DontShowIconsInMenus }),
    AA_NativeWindows                  = cpp!(const i32 as "int" { Qt::AA_NativeWindows }),
    AA_DontCreateNativeWidgetSiblings = cpp!(const i32 as "int" { Qt::AA_DontCreateNativeWidgetSiblings }),
    AA_PluginApplication              = cpp!(const i32 as "int" { Qt::AA_PluginApplication }),
    AA_DontUseNativeMenuBar           = cpp!(const i32 as "int" { Qt::AA_DontUseNativeMenuBar }),
    AA_MacDontSwapCtrlAndMeta         = cpp!(const i32 as "int" { Qt::AA_MacDontSwapCtrlAndMeta }),
    ...
}

It looks cumbersome, so it might be a better idea to support such scenario natively in cpp. Maybe via an additional cpp_enum! macro in complement to existing cpp_class!?

Implementation

I can think of three ways to implement this, varying by complexity level and overhead. Depending on range of supported data types, developing wire format might be required. But let's start with simple integer and floating types for now.

  1. Compile every const macro as a separate binary with its own main(). Communicate through stdout. Invoke separate binaries one by one.
    • Pros: simple and straightforward.
    • Cons: building and linking tens (probably hundreds) of micro-programs gonna take forever and a lot of space.
  2. Compile one binary with a switch in main() that chooses which const expression to return. Communicate through stdout, invoke one by one with different --const <name>.
    • Pros: only one binary, re-uses existing patterns.
    • Cons: Requires knowing all cpp! invocations in advance before substituting any of those. (I believe, this was implemented?)
  3. Single binary outputs everything at once.
    • Pros: Fast.
    • Const: Complex. Requires some wire format, at least to differentiate between expressions (which is which). Could be implemented with JSON Lines or just a JSON object/map.

All this would've been much easier if Rust would allow us to call FFI code within recently introduced inline const blocks. But that's an entirely different topic worth of RFC on its own.

Concerns

  • It is not clear as to what to do in case of cross compilation. Can it impose problems for such implementation? We need to compile and run code just as Cargo does that with build.rs. But unlike rustup/cargo, C++ toolchain is probably fixed for the target triple, so resulting binaries might not even run on a build machine.
  • Diagnostics is probably going to be like in the rest of cpp crate: panic if any of this fails, preferably with reasonable error messages. Compile time evaluation of C++ code, however, introduces new type of errors.
  • Where to put unsafe keyword and whether it is needed at all? In line with Rust function definitions, I suppose it would be cpp!(const unsafe ...).
  • In Rust adding const qualifier to a function does not change its further syntax, but cpp! removes arguments list, return keyword and ; semicolon.

Alternatives

Shared Types chapter in CXX crate: https://cxx.rs/shared.html.

@ogoffart
Copy link
Collaborator

ogoffart commented Jul 1, 2021

I think for the enum use case, the best is to use the bindgen tool.

But apart from that, this can still be useful.

For the implementation, it kind of sounds logical that this should be added in the metadata array, and the cpp! macro could just look for it. That limits us to usize though, but it could similarly be extended for other types.
(ie: Don't create a binary for each one, that would be too much overhead. We would just add some constexpr in the metadata)

Regarding your concerns:

It is not clear as to what to do in case of cross compilation.

Not a problem, this is just a constexpr and it will be evaluated at compile time on the host machine following the rules of constexpr

Diagnostics is probably going to be like in the rest of cpp crate: panic if any of this fails, preferably with reasonable error messages. Compile time evaluation of C++ code, however, introduces new type of errors.

It is not a new type of error. It is simply a C++ compilation error as any other.
These in itself could be improved, but that's another topic.

Where to put unsafe keyword and whether it is needed at all?

unsafe is not required since it is purely compile-time.

In Rust adding const qualifier to a function does not change its further syntax, but cpp! removes arguments list, return keyword and ; semicolon.

I think that's fine.

Alternatives: Shared Types chapter in CXX crate: https://cxx.rs/shared.html.

That's useful to declare an enum in your rust code that's usable from C++, but if you want to get the value from C++ enum from third party library, as i said before, i recommend the bindgen tool.
(Example in https://github.com/sixtyfpsui/sixtyfps/blob/master/sixtyfps_runtime/rendering_backends/qt/key_generated.rs#L12)

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

No branches or pull requests

2 participants