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

corrosion_add_cxxbridge: Allow circular dependencies between bridge/Rust/C++ #580

Merged

Conversation

LeonMatthesKDAB
Copy link
Contributor

Enforcing a link between the bridge and the crate has turned out
to be problematic if the dependency between the C++ and Rust code is
circular.

Example:
A reasonable way to build a CXX-enabled crate is to build 3 static
libraries:

  1. my_crate_cpp - contains the C++ code, which may access Rust via CXX
  2. my_crate_rust - contains the Rust code, which may access C++ via CXX
  • Note here that corrosion creates an additional my_crate_rust-static
    target for the actual static library, my_crate_rust is an INTERFACE
    target that links to this -static target.
  1. my_crate_bridge - bridge code (generated via corrosion_add_cxxbridge)

As the point of CXX is to freely communicate between C++ and Rust, these
3 libraries sort off act as one in the end and the dependencies between
them may be circular.

In CMake 3.24+, cirular dependencies can be resolved via a LINK_GROUP.
With corrosion, the right call would be:

target_link_libraries(my_crate_rust INTERFACE
    "$<LINK_GROUP:RESCAN,my_crate_cpp,my_crate_bridge,my_crate_rust-static>"
)

Which would allow C++ to call Rust feely and vice-versa.
The result would be an interface target my_crate_rust, which corresponds
to the Rust crate imported by corrosion and contains all 3 static
libraries.

However, this only works when removing the target_link_libraries call
from the bridge target to the crate target.
As otherwise, CMake complains that it cannot resolve the dependencies,
as the bridge already depends on the _rust target, which also already
depends on the _rust-static target, etc.
We also cannot create a LINK_GROUP that includes the my_crate_rust
target, as link groups are only allowed on STATIC library targets, and
the my_crate_rust is an INTERFACE target.

So this either needs to be left up to the user, or the
target_link_libraries call must be changed to link to the -static
target, instead of the INTERFACE target, as that is compatible with the
LINK_GROUP call.

@LeonMatthesKDAB LeonMatthesKDAB force-pushed the cxx-disable-link-libraries branch 2 times, most recently from 6693b5d to f26b053 Compare November 20, 2024 14:30
@LeonMatthesKDAB LeonMatthesKDAB changed the title Remove target_link_libraries in add_cxxbridge corrosion_add_cxxbridge: Remove target_link_libraries call Nov 20, 2024
@jschwe
Copy link
Collaborator

jschwe commented Nov 23, 2024

Enforcing a link between the bridge and the crate has turned out
to be problematic if the dependency between the C++ and Rust code is
circular.

Is this also a problem when using lld?
Does this problem also affect shared libraries?
Could you extend the exisiting cxxbridge testcases or add a new one to demonstrate / test the problem and prevent future regressions?

So this either needs to be left up to the user, or the
target_link_libraries call must be changed to link to the -static
target, instead of the INTERFACE target, as that is compatible with the
LINK_GROUP call.

Wouldn't the second option be preferable here? We could check if ${crate-name}-<static|shared> targets exist and then link to those

@LeonMatthesKDAB
Copy link
Contributor Author

Is this also a problem when using lld? Does this problem also affect shared libraries? Could you extend the exisiting cxxbridge testcases or add a new one to demonstrate / test the problem and prevent future regressions?

Unfortunately I don't know on which linkers specifically the argument order is important, I believe e.g. mold doesn't have this issue, but I'm unsure about lld.
For ld, which is still the default on many linux versions unfortunately, it's definitely a problem.

I think dynamic libraries may be able to sort this issue out, as they're resolving dependencies at runtime, but I haven't tried this

Wouldn't the second option be preferable here? We could check if ${crate-name}-<static|shared> targets exist and then link to those

It might well be. I will add another commit to try this.

@LeonMatthesKDAB LeonMatthesKDAB force-pushed the cxx-disable-link-libraries branch from 3f96705 to da33960 Compare November 27, 2024 15:29
@LeonMatthesKDAB
Copy link
Contributor Author

@jschwe
Please see my new commits for the updated version.
The test suite now has a cxxbridge_circular test, which tests this setup.
That commit also includes an addition fix.

I'm somewhat hesitant about adding back the target_link_libraries call...
It can be convenient, but also cannot be undone after the call to corrosion_add_cxxbridge.
And in some cases (like cpp2rust and the circular case), it's not needed anyway.
With the current setup it hopefully doesn't break anything, but it's a bit strange that you have to manually link the cpp library to the bridge, but not the other way around.
To me it feels like this should be explicit.

Anyway, It's of course your decision @jschwe .
Thank you for this project 🥇

Enforcing a link between the bridge and the crate has turned out
to be problematic if the dependency between the C++ and Rust code is
circular.

Example:
A reasonable way to build a CXX-enabled crate is to build 3 static
libraries:
1. my_crate_cpp - contains the C++ code, which may access Rust via CXX
2. my_crate_rust - contains the Rust code, which may access C++ via CXX
  * Note here that corrosion creates an additional my_crate_rust-static
    target for the actual static library, my_crate_rust is an INTERFACE
    target that links to this -static target.
3. my_crate_bridge - bridge code (generated via corrosion_add_cxxbridge)

As the point of CXX is to freely communicate between C++ and Rust, these
3 libraries sort off act as one in the end and the dependencies between
them may be circular.

In CMake 3.24+, cirular dependencies can be resolved via a LINK_GROUP.
With corrosion, the right call would be:

```cmake
target_link_libraries(my_crate_rust INTERFACE
    "$<LINK_GROUP:RESCAN,my_crate_cpp,my_crate_bridge,my_crate_rust-static>"
)
```

Which would allow C++ to call Rust feely and vice-versa.
The result would be an interface target my_crate_rust, which corresponds
to the Rust crate imported by corrosion and contains all 3 static
libraries.

However, this only works when removing the target_link_libraries call
from the bridge target to the crate target.
As otherwise, CMake complains that it cannot resolve the dependencies,
as the bridge already depends on the _rust target, which also already
depends on the `_rust-static` target, etc.
We also cannot create a LINK_GROUP that includes the my_crate_rust
target, as link groups are only allowed on STATIC library targets, and
the my_crate_rust is an INTERFACE target.

So this either needs to be left up to the user, or the
target_link_libraries call must be changed to link to the `-static`
target, instead of the INTERFACE target, as that is compatible with the
LINK_GROUP call.
@LeonMatthesKDAB LeonMatthesKDAB force-pushed the cxx-disable-link-libraries branch 6 times, most recently from 5798bb2 to ee6ba2b Compare November 27, 2024 16:34
This includes a fix in corrosion_add_cxxbridge, to force generation
of the header files for all upstream dependencies, which is important in
this circular setup.
This is a convenience for the user, as this is usually needed anyway.
@LeonMatthesKDAB LeonMatthesKDAB force-pushed the cxx-disable-link-libraries branch from ee6ba2b to d3d27c4 Compare November 27, 2024 16:40
@jschwe
Copy link
Collaborator

jschwe commented Dec 2, 2024

Sorry for the slow review.

The test suite now has a cxxbridge_circular test, which tests this setup.

Thanks!

I'm somewhat hesitant about adding back the target_link_libraries call...
It can be convenient, but also cannot be undone after the call to corrosion_add_cxxbridge.
And in some cases (like cpp2rust and the circular case), it's not needed anyway.

We could add an option to corrosion_add_cxxbridge for disabling the call. I don't feel super strong about this though. But if we wanted to remove the target_link_libraries call, then we would need to improve the documentation.

With the current setup it hopefully doesn't break anything, but it's a bit strange that you have to manually link the cpp library to the bridge, but not the other way around.

Yeah, that is indeed a bit strange. How about we merge this PR as-is for now? We could still discuss whether we should call target_link_libraries by default in a separate issue / PR.

@LeonMatthesKDAB
Copy link
Contributor Author

@jschwe Thank you for the review :)

From my side, I'm okay to merge this PR with the link_libraries call included.
It's definitely the least invasive change. I'll rename the PR accordingly.
If this causes any further issues I'll report back and create a follow-up PR.

@LeonMatthesKDAB LeonMatthesKDAB changed the title corrosion_add_cxxbridge: Remove target_link_libraries call corrosion_add_cxxbridge: Allow circular dependencies between bridge/Rust/C++ Dec 4, 2024
@jschwe jschwe merged commit 934fee2 into corrosion-rs:master Dec 4, 2024
40 checks passed
@LeonMatthesKDAB
Copy link
Contributor Author

Hi @jschwe ,
just wanted to report that I have found a weird side-effect of exposing the generated headers as PUBLIC sources of the bridge.

While I still think this is the right thing to do, this change caused an error in a downstream dependency that one of the generated header files could not be found:

- CMake Error at ...../CMakeLists.txt:61 (target_link_libraries):
-  Cannot find source file:
-
-    ...../corrosion_generated/..../somefile.h
-
-  Tried extensions .c .C .c++ .cc .cpp .cxx .cu .mpp .m .M .mm .ixx .cppm
-  .ccm .cxxm .c++m .h .hh .h++ .hm .hpp .hxx .in .txx .f .F .for .f77 .f90
-  .f95 .f03 .hip .ispc

This is strange because CMake should know that this file is generated, and that this therefore shouldn't be an issue.

It turned out that in older versions of CMake the GENERATED property isn't correctly propagated.
See also: https://gitlab.kitware.com/cmake/cmake/-/issues/18399
This has since been fixed with CMake 3.20: https://cmake.org/cmake/help/v3.20/policy/CMP0118.html

So if anyone encounters this issue, it's easy enough to fix by calling:
cmake_minimium_required(VERSION 3.20 FATAL_ERROR) (or any other version above 3.20)
As described here, this implies a call to cmake_policy which enables CMP0118.

Unfortunately this must be done in all (transitive) downstream dependencies CMakeLists.txt that link to the bridge target, so cannot be done from within corrosion.

I hope this won't cause too much trouble...

@jschwe
Copy link
Collaborator

jschwe commented Dec 6, 2024

@LeonMatthesKDAB Could you perhaps open a PR to add the error and solution to the Common issues page in our book? The source is under doc/src/common_issues.md.

LeonMatthesKDAB added a commit to LeonMatthesKDAB/corrosion that referenced this pull request Dec 18, 2024
As described in corrosion-rs#580, this error may be encountered when using
corrosion_add_cxxbridge, now that the generated headers are added as
`PUBLIC` sources.
@LeonMatthesKDAB
Copy link
Contributor Author

@jschwe done, see: #586

jschwe pushed a commit that referenced this pull request Dec 18, 2024
As described in #580, this error may be encountered when using
corrosion_add_cxxbridge, now that the generated headers are added as
`PUBLIC` sources.
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.

2 participants