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

Use cxx-rs for core.rs #2336

Merged
merged 5 commits into from
Dec 23, 2020
Merged

Conversation

cgwalters
Copy link
Member

@cgwalters cgwalters commented Nov 25, 2020

This is much better than bindgen because it's fully safe. It's
much more ergonomic too:

  • Invoke Rust methods-on-structs just like C++ methods-on-structs
  • Rust Result<> is translated automatically to exceptions

See https://cxx.rs/context.html for more.

@cgwalters cgwalters marked this pull request as draft November 25, 2020 19:45
@cgwalters cgwalters marked this pull request as ready for review November 27, 2020 17:54
@cgwalters cgwalters force-pushed the cxx-rs branch 2 times, most recently from c816c67 to bd0cd00 Compare November 27, 2020 19:56
@cgwalters
Copy link
Member Author

Looks like CI fails here because Rust is too old in f32. Hopefully we get coreos/coreos-assembler#1881 going soon.

glnx_autofd int fd = -1;
try {
fd = rpmostreecxx::download_to_fd(*pkg);
} catch (std::exception& e) {
Copy link
Member Author

@cgwalters cgwalters Nov 30, 2020

Choose a reason for hiding this comment

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

This one is a really interesting topic. GError is closer to Rust's explicit Result than C++ exceptions are. However...for most of the usage of GError in rpm-ostree we are effectively just using it to unwind up the stack - i.e. we don't explicitly catch errors.

The only exceptions¹ I can think of are:

  • main() (obviously)
  • GDBus method handlers

And so what I'd probably argue for is that rather than doing try/catch-to-gerror at places like this we just go with the C++ exception flow, and install toplevel try/catch at those places that need it.

(Also in rpm-ostree the usage of g_error_matches() is something like 90% for ENOENT...which totally validates https://github.com/cgwalters/openat-ext/blob/26324991dd1648bab64ce1f2e6471186e6ed21f9/src/lib.rs#L36 )

¹ 😀

Copy link
Member

Choose a reason for hiding this comment

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

Now that main() has a try/catch, I think can drop this one, right?

@cgwalters
Copy link
Member Author

OK I looked at this a bit more. Definitely a whole mixed bag of tradeoffs here.

Idiomatic C++ strings and (not to mention Rust String) do totally clash with our pervasive use of C NUL-terminated strings; see e.g. https://www.murrayc.com/permalink/2017/06/20/c-stdstring_view-not-so-useful-when-calling-c-functions/

We are already using CUtf8Buf, so...we could use that more Rust side. Hmm, and perhaps a viable path here is adding support for that crate into cxx-rs as an optional feature (or perhaps just move it into cxx-rs?).

The other thing I haven't tried here but is probably going to be annoying is supporting passing GObjects.

Hmm. So one option I mentioned in our chat is we could fork cxx and add custom stuff. Since some problem domains here are kind of unique to us it's not totally crazy IMO. Particularly since eventually we wouldn't need it.

@cgwalters cgwalters force-pushed the cxx-rs branch 2 times, most recently from 9afd13f to 73e7405 Compare December 3, 2020 22:22
@cgwalters
Copy link
Member Author

OK right, currently our Prow job is using SKIP_INSTALLDEPS=1 so it was missing the hack to pull in cxxbridge-cmd. But actually...to ship we need to vendor that just like we do bindgen.

@cgwalters
Copy link
Member Author

And the Jenkins/CoreOS CI is blocked on coreos/coreos-assembler#1881 because the Fedora team isn't backporting newer Rust to 32, and it looks like cxxbridge gained a dep on very recent Rust for some const fn stuff.

@cgwalters
Copy link
Member Author

OK cool after filing dtolnay/cxx#544 I figured out how to pass through GObjects, which I think was the biggest blocker to this.

@cgwalters
Copy link
Member Author

OK I reworked this to have a better demo; I converted @lucab 's work in core.rs. Note how all the unsafe wrappers drop out (I replaced that section with unit tests 😄 ) and how we can natively invoke methods from the C++ side on the Rust object that throws a Result.

/// Remove the temporary /etc, and destroy the guard.
pub fn redo_usretc(self) -> anyhow::Result<()> {
pub fn undo(&self) -> anyhow::Result<()> {
Copy link
Contributor

Choose a reason for hiding this comment

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

From an out-of-band chat: this seems to be a cxx limitation, not being able to consume the ownership of the object itself.

/// Remove the temporary /etc, and destroy the guard.
pub fn redo_usretc(self) -> anyhow::Result<()> {
pub fn undo(&self) -> anyhow::Result<()> {
Copy link
Member

Choose a reason for hiding this comment

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

Would be cool if we could we make the undo happen in Drop::drop so we don't have to explicitly call it. This matches some other guards we have in C which used autocleanups to "unguard".

Copy link
Member Author

Choose a reason for hiding this comment

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

We could do that actually! But I explicitly chose not to because there's no way to check for errors in drop(). (This gets into the whole rationale behind things like https://docs.rs/drop_bomb/0.1.5/drop_bomb/ that we use in https://docs.rs/openat-ext/0.1.9/openat_ext/struct.FileWriter.html )

@@ -4576,8 +4575,7 @@ rpmostree_context_assemble (RpmOstreeContext *self,
}

/* Undo the /etc move above */
if (!ror_tempetc_redo_usretc (etc_guard, error))
return FALSE;
etc_guard->undo();
Copy link
Member Author

@cgwalters cgwalters Dec 11, 2020

Choose a reason for hiding this comment

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

I want to emphasize how representative this example will be I think of future code. Notice that we start using C++ method invocation syntax on a struct, and we use C++ exceptions instead of GError.

Personally I think the first is a notable win (on top of how we didn't need to manually create unsafe C wrappers). I'm less happy with C++ exceptions but I think on balance if we can greatly accelerate oxidation it will be worth the short term hit.

@cgwalters
Copy link
Member Author

OK this needs #2381

@cgwalters cgwalters changed the title WIP demo of using cxx-rs Use cxx-rs for core.rs Dec 11, 2020
@cgwalters
Copy link
Member Author

OK, lifting draft on this finally!

@cgwalters
Copy link
Member Author

OK, fixed the thinko I introduced when pulling the utils.rs code out of another branch; now passing CI!

<!-- Generated with: fakeroot /bin/sh -c 'cd dracut-urandom && find . -print0 | sort -z | (mknod dev/random c 1 8 && mknod dev/urandom c 1 9 && cpio -o --null -H newc -R 0:0 --reproducible --quiet -D . -O /tmp/dracut-urandom.cpio)' -->
<file>dracut-random.cpio.gz</file>
</gresource>
</gresources>
Copy link
Member Author

Choose a reason for hiding this comment

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

100% less XML!

if (lseek (tmpf.fd, 0, SEEK_END) < 0)
return glnx_throw_errno_prefix (error, "lseek");
if (glnx_loop_write (tmpf.fd, random_cpio_data_p, random_cpio_data_len) < 0)
if (glnx_loop_write (tmpf.fd, random_cpio_data.data(), random_cpio_data.length()) < 0)
Copy link
Member Author

@cgwalters cgwalters Dec 22, 2020

Choose a reason for hiding this comment

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

I know the parenthesis spacing is inconsistent. It's a challenge mentally for me at least because the methods being shared with Rust pulls towards Rust style. And in particular for method invocations there's a strong "binding" between the identifier and its arguments. Most C++ projects don't use a space between method name and open paren; there isn't an offical GNU coding style for C++ AFAICS.

Overall this kind of fits in with a theme of having the C/C++ get a bit uglier in some cases in return for making it way easier to bridge to Rust.

Also, perhaps at some point we can investigate using clang-format.

In the previous buildsytem rework we disabled the unit tests because
of linking problems.  Now I realized that a simple solution is
to continue to build one big object, just make it an internal
static library and have a tiny "stub main" that delegates to an entrypoint.

That's basically what the C unit tests are - an alternative `main()`
with some extra code.
We need to cleanly split off "test dependencies" that we
install inside the cosa pod from builds (where we won't
have `cargo`) from the build time where we use the cosa
buildroot image.

Prep for using https://cxx.rs
This is much better than bindgen because it's fully safe.  It's
much more ergonomic too:

 - Invoke Rust methods-on-structs just like C++ methods-on-structs
 - Rust `Result<>` is translated automatically to exceptions

See https://cxx.rs/context.html for more.
A step towards converting all of utils.
The way gresources work using a constructor function started
failing when I was refactoring the build system, and I couldn't
figure it out.  It's just easier to use Rust for this which
has nice toolchain-integrated functionality for this.
@cgwalters
Copy link
Member Author

Rebased 🏄 this one, not because it really requires it, but because I have further branches on top of this and they benefit from the rest of the C++ porting.

Copy link
Member

@jlebon jlebon left a comment

Choose a reason for hiding this comment

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

Cool stuff, approach LGTM overall!

@@ -10,11 +10,23 @@
mod ffiutil;
mod includes;

#[cxx::bridge(namespace = "rpmostreecxx")]
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't the namespace name be e.g. rpmostreers to reflect the fact that it's actually calling into Rust code?

Copy link
Member Author

@cgwalters cgwalters Dec 22, 2020

Choose a reason for hiding this comment

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

Ah but see it's bidirectional, cxx supports Rust calling C++ too and I just submitted #2413 to demo that.
Concretely anything listed in extern "C++" is expected to be in the target namespace too.

That said it could totally use a better name.

  • ror:: following the ror_ naming for bindgen?
  • rpmostree:: - Unimaginative but obvious
  • Some made up name based on...a bridge or something? skytrain:: ?

Copy link
Member Author

Choose a reason for hiding this comment

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

OTOH we can also change the name later; there's not a lot of code using this right now. It'll just get slightly more expensive to do so as usage increases.

Copy link
Member Author

Choose a reason for hiding this comment

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

I looked at this a bit more. It's an interesting topic; partly comes down to whether we see the "cxxrs" part as separate from the C/C++ part. And the core question there is, do we stick everything in the C++ side into the same rpmostree namespace? That'd mean that it'd just be e.g. download_to_fd and not rpmostreecxx::download_to_fd() when called.

This also conceptually would lead towards dropping the C function prefixing, e.g. instead of rpmostree_syscore_bump_mtime() it'd be rpmostree::syscore_bump_mtime(). My feeling right now is that'd be way too much churn; the high level goal here is to sprint towards thinning out the C/C++ side into Rust, not to spend time rewriting into "modern C++". But there will likely be some of the latter.

So I think for now let's just keep rpmostreecxx:: (and not stick any C++ code into a new namespace) because it highlights the C++/Rust-bridged code in our C-in-C++ code which just works differently for now.

Copy link
Member

Choose a reason for hiding this comment

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

Right OK, got it. WDYT about cxxrs::? Anyway, yeah we can revisit later.

Also, definitely agreed re. not trying to make too much use of namespaces outside of cxxrs.

glnx_autofd int fd = -1;
try {
fd = rpmostreecxx::download_to_fd(*pkg);
} catch (std::exception& e) {
Copy link
Member

Choose a reason for hiding this comment

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

Now that main() has a try/catch, I think can drop this one, right?

@cgwalters
Copy link
Member Author

cgwalters commented Dec 22, 2020

Now that main() has a try/catch, I think can drop this one, right?

This one is still necessary to print failed; notice in this update though rather than converting to GError we just re-throw;.

@cgwalters
Copy link
Member Author

BTW here's a recent blog on cxx in a KDE project.

@jlebon
Copy link
Member

jlebon commented Dec 23, 2020

/lgtm

@openshift-ci-robot
Copy link
Collaborator

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: cgwalters, jlebon

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-merge-robot openshift-merge-robot merged commit 6579ab7 into coreos:master Dec 23, 2020
# Install build dependencies not in the cosa buildroot already
set -xeuo pipefail
if ! command -v cxxbridge; then
cargo install --root=/usr cxxbridge-cmd
Copy link
Member

Choose a reason for hiding this comment

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

Does this actually need to be in /usr BTW? Running this script locally.

Copy link
Member Author

@cgwalters cgwalters Dec 23, 2020

Choose a reason for hiding this comment

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

Hmm, I guess we could dispatch on $(id -u)?

@Mershl
Copy link
Contributor

Mershl commented Dec 28, 2020

Seeing the following errors on a fresh container and master:

src/app/rpmostree-dbus-helpers.cxx: In functiongboolean rpmostree_sort_pkgs_strv(const char* const*, GUnixFDList*, GPtrArray**, GVariant**, GError**)’:
src/app/rpmostree-dbus-helpers.cxx:1069:18: error: ‘rpmostreecxxhas not been declared
 1069 |             fd = rpmostreecxx::download_to_fd (pkg);
      |                  ^~~~~~~~~~~~
src/app/rpmostree-dbus-helpers.cxx:1070:25: error: ‘exceptionin namespacestddoes not name a type
 1070 |           } catch (std::exception& e) {
      |                         ^~~~~~~~~
make[2]: *** [Makefile:3398: src/app/librpmostreeinternals_la-rpmostree-dbus-helpers.lo] Error 1

Don't know the codebase well enough for (1).
Explicitly including <exception> fixes (2).

EDIT: Fixing (1) yields more errors of the same nature. Is master/or me missing a common include?

src/libpriv/rpmostree-core.cxx: In functiongboolean rpmostree_context_assemble(RpmOstreeContext*, GCancellable*, GError**)’:
src/libpriv/rpmostree-core.cxx:4326:20: error: ‘rpmostreecxxhas not been declared
 4326 |   auto etc_guard = rpmostreecxx::prepare_tempetc_guard (tmprootfs_dfd);
      |                    ^~~~~~~~~~~~
src/libpriv/rpmostree-core.cxx:4372:36: error: ‘rpmostreecxxhas not been declared
 4372 |           auto systemctl_wrapper = rpmostreecxx::get_systemctl_wrapper ();
      |   

@cgwalters
Copy link
Member Author

In some cases you may need to make clean after doing an update - the buildsystem doesn't correctly reflect the interdependencies between the C/C++ side and Rust (because if it did every Rust change would trigger a C++ build right now).
Or a more precise change is usually: rm -f rpmostree-rust.h rpmostree-cxxrs*.

@Mershl
Copy link
Contributor

Mershl commented Dec 29, 2020

Thanks for your suggestion @cgwalters. I've ran make clean before which fixed cxxbridge: command not found. Unfortunatly the main branch is still not building for me, with the errors above.

Commands issued in a F33 build container:

git clone https://github.com/coreos/rpm-ostree.git
cd rpm-ostree/
git submodule update --init
env NOCONFIGURE=1 ./autogen.sh
./configure --prefix=/usr --libdir=/usr/lib64 --sysconfdir=/etc
make clean
make

Side suggestion: Let's add make clean as a suggestion before make to https://coreos.github.io/rpm-ostree/HACKING/#raw-build-instructions. Else cxxbridge: command not found will be visible until cleaned.

@cgwalters cgwalters added the project/c++ Porting to C++ label Mar 21, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants