From c2bae19041442a650c1158e1c01db98226b0ef55 Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Sat, 5 Oct 2024 10:37:38 -0500 Subject: [PATCH] Add WIP on musl C++ support --- .github/workflows/test.yml | 66 +++++++++++++- action.yml | 4 + musl-symlink.sh | 86 +++++++++++++++++++ .../pure-rust}/Cargo.lock | 0 .../pure-rust}/Cargo.toml | 0 .../pure-rust}/src/bin1.rs | 0 .../pure-rust}/src/bin2.rs | 0 .../pure-rust}/subcrate/Cargo.lock | 0 .../pure-rust}/subcrate/Cargo.toml | 0 .../pure-rust}/subcrate/src/main.rs | 0 test-projects/with-cxx/Cargo.toml | 15 ++++ test-projects/with-cxx/README.md | 1 + test-projects/with-cxx/build.rs | 10 +++ test-projects/with-cxx/include/blobstore.h | 26 ++++++ test-projects/with-cxx/src/blobstore.cc | 71 +++++++++++++++ test-projects/with-cxx/src/main.rs | 59 +++++++++++++ 16 files changed, 334 insertions(+), 4 deletions(-) create mode 100755 musl-symlink.sh rename {test-project => test-projects/pure-rust}/Cargo.lock (100%) rename {test-project => test-projects/pure-rust}/Cargo.toml (100%) rename {test-project => test-projects/pure-rust}/src/bin1.rs (100%) rename {test-project => test-projects/pure-rust}/src/bin2.rs (100%) rename {test-project => test-projects/pure-rust}/subcrate/Cargo.lock (100%) rename {test-project => test-projects/pure-rust}/subcrate/Cargo.toml (100%) rename {test-project => test-projects/pure-rust}/subcrate/src/main.rs (100%) create mode 100644 test-projects/with-cxx/Cargo.toml create mode 100644 test-projects/with-cxx/README.md create mode 100644 test-projects/with-cxx/build.rs create mode 100644 test-projects/with-cxx/include/blobstore.h create mode 100644 test-projects/with-cxx/src/blobstore.cc create mode 100644 test-projects/with-cxx/src/main.rs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e2e188f..a858c96 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,8 +5,8 @@ on: pull_request: jobs: - test: - name: Test + test-pure-rust: + name: Test a pure Rust project strategy: fail-fast: false matrix: @@ -223,8 +223,8 @@ jobs: - name: Copy test project to root shell: bash run: | - cp -a test-project/* . - rm -fr test-project + cp -a test-projects/pure-rust/* . + rm -fr test-projects - name: Run both commands uses: ./ with: @@ -271,3 +271,61 @@ jobs: --expect-cross-version "${{ matrix.platform.expect_cross_version }}" \ ${{ matrix.platform.expect_cross }} \ ${{ matrix.platform.expect_stripped }} + + test-cxx: + name: Test a project with C++ + strategy: + fail-fast: false + matrix: + os: + - ubuntu-20.04 + - ubuntu-22.04 + - ubuntu-24.04 + runs-on: ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Copy test project to root + shell: bash + run: | + cp -a test-projects/with-cxx/* . + rm -fr test-projects + - name: Run both commands + uses: ./ + with: + command: both + cache-cross-binary: true + target: x86_64-unknown-linux-musl + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Run test command + uses: ./ + with: + command: test + cache-cross-binary: true + target: x86_64-unknown-linux-musl + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Run build command + uses: ./ + with: + command: build + cache-cross-binary: true + target: x86_64-unknown-linux-musl + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + strip: true + - name: Run build command for subdir + uses: ./ + with: + command: build + cache-cross-binary: true + working-directory: subcrate + target: x86_64-unknown-linux-musl + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + strip: true + - name: Check binary and cross on ${{ matrix.os }} + shell: bash + run: | + tests/check-binary.pl \ + --target "x86_64-unknown-linux-musl" \ + --expect-file-re "ELF.+x86-64" \ + --no-expect-cross \ + --expect-stripped diff --git a/action.yml b/action.yml index 545b70a..431ad61 100644 --- a/action.yml +++ b/action.yml @@ -87,6 +87,10 @@ runs: shell: bash run: sudo apt-get update --yes && sudo apt-get install --yes musl-tools if: steps.determine-cross-compile.outputs.needs-cross != 'true' && contains(inputs.target, 'musl') + - name: Set up system for C++ with musl + shell: bash + run: musl-symlink.sh + if: steps.determine-cross-compile.outputs.needs-cross != 'true' && contains(inputs.target, 'musl') - name: Set build command id: set-build-command shell: bash diff --git a/musl-symlink.sh b/musl-symlink.sh new file mode 100755 index 0000000..ebc13ec --- /dev/null +++ b/musl-symlink.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash + +# This script was copied from the https://github.com/cross-rs/cross project at commit +# ac4c11cedc97cd7c27faed36e55377a90e6ed618. The license for that project in its `Cargo.toml` file is +# "MIT OR Apache-2.0". The copyright in the MIT license says: +# +# Copyright (c) 2017-2022 by the respective authors +# Copyright (c) 2016 Jorge Aparicio +# +# The script was adjusted to get rid of the sysroot and arch parameters in favor of hardcoding the +# paths for Ubuntu 22.04 and the x86-64 arch. + +# Create necessary symlinks for musl images to run +# dynamically-linked binaries. +# Just to be careful, we need this in a few locations, +# relative to the musl sysroot. +# /lib/ld-musl-armhf.so +# /lib/ld-musl-armhf.so.1 +# /usr/lib/ld.so +# /usr/lib/ld.so.1 +# /usr/lib/libc.so +# /usr/lib/libc.so.1 + +set -x +set -euo pipefail + +main() { + apt-get --yes install libboost-dev + + local src + local dst + local dsts + local libstdcpp_path + local libstdcpp_basename + + # ignore any failures here + src="/usr/lib/x86_64-linux-musl/libc.so" + dsts=( + "/usr/lib/ld-musl-x86_64.so" + "/usr/lib/ld-musl-x86_64.so.1" + "/usr/lib/ld.so" + "/usr/lib/ld.so.6" + "/usr/lib/libc.so" + "/usr/lib/libc.so.6" + ) + for dst in "${dsts[@]}"; do + # force a link if the dst does not exist or is broken + if [[ -L ${dst} ]] && [[ ! -e ${dst} ]]; then + ln -sf "${src}" "${dst}" + elif [[ ! -f ${dst} ]]; then + ln -s "${src}" "${dst}" + fi + done + + libstdcpp_path=$(find /usr/lib/x86_64-linux-gnu/ -name 'libstdc++.so.6.0.*') + libstdcpp_basename=$(basename "$libstdcpp_path") + + # ensure we statically link libstdc++, so avoid segfaults with c++ + # https://github.com/cross-rs/cross/issues/902 + find /usr -name 'libstdc++.so*' -exec rm -f {} \; + + # now, we create a linker script that adds all the required dependencies + # because we link to a static libstdc++ to avoid runtime issues and + # with the shared libstdc++, we can have missing symbols that are referenced + # in libstdc++, such as those from libc like `setlocale` and `__cxa_atexit`, + # as well as those from libgcc, like `__extendsftf2`. all musl targets + # can require symbols from libc, however, only the following are known + # to require symbols from libgcc: + # - aarch64-unknown-linux-musl + # - mips64-unknown-linux-muslabi64 + # - mips64el-unknown-linux-muslabi64 + echo '/* cross-rs linker script + * this allows us to statically link libstdc++ to avoid segfaults + * https://github.com/cross-rs/cross/issues/902 + */ +GROUP ( libstdc++.a AS_NEEDED( -lgcc -lc -lm ) ) +' >"$libstdcpp_path" + ln -s "$libstdcpp_basename" /usr/lib/x86_64-linux-gnu/libstdc++.so.6 + ln -s "$libstdcpp_basename" /usr/lib/x86_64-linux-gnu/libstdc++.so + + echo /usr/lib/x86_64-linux-gnu >>/etc/ld-musl-x86_64.path + + ln -s /usr/bin/musl-gcc /usr/bin/musl-g++ +} + +main diff --git a/test-project/Cargo.lock b/test-projects/pure-rust/Cargo.lock similarity index 100% rename from test-project/Cargo.lock rename to test-projects/pure-rust/Cargo.lock diff --git a/test-project/Cargo.toml b/test-projects/pure-rust/Cargo.toml similarity index 100% rename from test-project/Cargo.toml rename to test-projects/pure-rust/Cargo.toml diff --git a/test-project/src/bin1.rs b/test-projects/pure-rust/src/bin1.rs similarity index 100% rename from test-project/src/bin1.rs rename to test-projects/pure-rust/src/bin1.rs diff --git a/test-project/src/bin2.rs b/test-projects/pure-rust/src/bin2.rs similarity index 100% rename from test-project/src/bin2.rs rename to test-projects/pure-rust/src/bin2.rs diff --git a/test-project/subcrate/Cargo.lock b/test-projects/pure-rust/subcrate/Cargo.lock similarity index 100% rename from test-project/subcrate/Cargo.lock rename to test-projects/pure-rust/subcrate/Cargo.lock diff --git a/test-project/subcrate/Cargo.toml b/test-projects/pure-rust/subcrate/Cargo.toml similarity index 100% rename from test-project/subcrate/Cargo.toml rename to test-projects/pure-rust/subcrate/Cargo.toml diff --git a/test-project/subcrate/src/main.rs b/test-projects/pure-rust/subcrate/src/main.rs similarity index 100% rename from test-project/subcrate/src/main.rs rename to test-projects/pure-rust/subcrate/src/main.rs diff --git a/test-projects/with-cxx/Cargo.toml b/test-projects/with-cxx/Cargo.toml new file mode 100644 index 0000000..f125cf2 --- /dev/null +++ b/test-projects/with-cxx/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "demo" +version = "0.0.0" +authors = ["David Tolnay "] +description = "Toy project from https://github.com/dtolnay/cxx" +edition = "2021" +license = "MIT OR Apache-2.0" +publish = false +repository = "https://github.com/dtolnay/cxx" + +[dependencies] +cxx = "1.0" + +[build-dependencies] +cxx-build = "1.0" diff --git a/test-projects/with-cxx/README.md b/test-projects/with-cxx/README.md new file mode 100644 index 0000000..58ff9a9 --- /dev/null +++ b/test-projects/with-cxx/README.md @@ -0,0 +1 @@ +This was copied from https://github.com/dtolnay/cxx/tree/master/demo diff --git a/test-projects/with-cxx/build.rs b/test-projects/with-cxx/build.rs new file mode 100644 index 0000000..7e19892 --- /dev/null +++ b/test-projects/with-cxx/build.rs @@ -0,0 +1,10 @@ +fn main() { + cxx_build::bridge("src/main.rs") + .file("src/blobstore.cc") + .std("c++14") + .compile("cxxbridge-demo"); + + println!("cargo:rerun-if-changed=src/main.rs"); + println!("cargo:rerun-if-changed=src/blobstore.cc"); + println!("cargo:rerun-if-changed=include/blobstore.h"); +} diff --git a/test-projects/with-cxx/include/blobstore.h b/test-projects/with-cxx/include/blobstore.h new file mode 100644 index 0000000..d89583a --- /dev/null +++ b/test-projects/with-cxx/include/blobstore.h @@ -0,0 +1,26 @@ +#pragma once +#include "rust/cxx.h" +#include + +namespace org { +namespace blobstore { + +struct MultiBuf; +struct BlobMetadata; + +class BlobstoreClient { +public: + BlobstoreClient(); + uint64_t put(MultiBuf &buf) const; + void tag(uint64_t blobid, rust::Str tag) const; + BlobMetadata metadata(uint64_t blobid) const; + +private: + class impl; + std::shared_ptr impl; +}; + +std::unique_ptr new_blobstore_client(); + +} // namespace blobstore +} // namespace org diff --git a/test-projects/with-cxx/src/blobstore.cc b/test-projects/with-cxx/src/blobstore.cc new file mode 100644 index 0000000..7cf40df --- /dev/null +++ b/test-projects/with-cxx/src/blobstore.cc @@ -0,0 +1,71 @@ +#include "demo/include/blobstore.h" +#include "demo/src/main.rs.h" +#include +#include +#include +#include +#include + +namespace org { +namespace blobstore { + +// Toy implementation of an in-memory blobstore. +// +// In reality the implementation of BlobstoreClient could be a large complex C++ +// library. +class BlobstoreClient::impl { + friend BlobstoreClient; + using Blob = struct { + std::string data; + std::set tags; + }; + std::unordered_map blobs; +}; + +BlobstoreClient::BlobstoreClient() : impl(new class BlobstoreClient::impl) {} + +// Upload a new blob and return a blobid that serves as a handle to the blob. +uint64_t BlobstoreClient::put(MultiBuf &buf) const { + std::string contents; + + // Traverse the caller's chunk iterator. + // + // In reality there might be sophisticated batching of chunks and/or parallel + // upload implemented by the blobstore's C++ client. + while (true) { + auto chunk = next_chunk(buf); + if (chunk.size() == 0) { + break; + } + contents.append(reinterpret_cast(chunk.data()), chunk.size()); + } + + // Insert into map and provide caller the handle. + auto blobid = std::hash{}(contents); + impl->blobs[blobid] = {std::move(contents), {}}; + return blobid; +} + +// Add tag to an existing blob. +void BlobstoreClient::tag(uint64_t blobid, rust::Str tag) const { + impl->blobs[blobid].tags.emplace(tag); +} + +// Retrieve metadata about a blob. +BlobMetadata BlobstoreClient::metadata(uint64_t blobid) const { + BlobMetadata metadata{}; + auto blob = impl->blobs.find(blobid); + if (blob != impl->blobs.end()) { + metadata.size = blob->second.data.size(); + std::for_each(blob->second.tags.cbegin(), blob->second.tags.cend(), + [&](auto &t) { metadata.tags.emplace_back(t); }); + } + return metadata; +} + +std::unique_ptr new_blobstore_client() { + return std::make_unique(); +} + +} // namespace blobstore +} // namespace org diff --git a/test-projects/with-cxx/src/main.rs b/test-projects/with-cxx/src/main.rs new file mode 100644 index 0000000..458f1f2 --- /dev/null +++ b/test-projects/with-cxx/src/main.rs @@ -0,0 +1,59 @@ +#[cxx::bridge(namespace = "org::blobstore")] +mod ffi { + // Shared structs with fields visible to both languages. + struct BlobMetadata { + size: usize, + tags: Vec, + } + + // Rust types and signatures exposed to C++. + extern "Rust" { + type MultiBuf; + + fn next_chunk(buf: &mut MultiBuf) -> &[u8]; + } + + // C++ types and signatures exposed to Rust. + unsafe extern "C++" { + include!("demo/include/blobstore.h"); + + type BlobstoreClient; + + fn new_blobstore_client() -> UniquePtr; + fn put(&self, parts: &mut MultiBuf) -> u64; + fn tag(&self, blobid: u64, tag: &str); + fn metadata(&self, blobid: u64) -> BlobMetadata; + } +} + +// An iterator over contiguous chunks of a discontiguous file object. +// +// Toy implementation uses a Vec> but in reality this might be iterating +// over some more complex Rust data structure like a rope, or maybe loading +// chunks lazily from somewhere. +pub struct MultiBuf { + chunks: Vec>, + pos: usize, +} +pub fn next_chunk(buf: &mut MultiBuf) -> &[u8] { + let next = buf.chunks.get(buf.pos); + buf.pos += 1; + next.map_or(&[], Vec::as_slice) +} + +fn main() { + let client = ffi::new_blobstore_client(); + + // Upload a blob. + let chunks = vec![b"fearless".to_vec(), b"concurrency".to_vec()]; + let mut buf = MultiBuf { chunks, pos: 0 }; + let blobid = client.put(&mut buf); + println!("blobid = {}", blobid); + + // Add a tag. + client.tag(blobid, "rust"); + + // Read back the tags. + let metadata = client.metadata(blobid); + println!("tags = {:?}", metadata.tags); +}