diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..87ebea0 --- /dev/null +++ b/.clang-format @@ -0,0 +1,148 @@ +--- +Language: Cpp +AccessModifierOffset: -2 +AlignAfterOpenBracket: Align +AlignConsecutiveMacros: false +AlignConsecutiveAssignments: true +AlignConsecutiveBitFields: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Left +AlignOperands: Align +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: true +AllowAllConstructorInitializersOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortEnumsOnASingleLine: true +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortLambdasOnASingleLine: All +AllowShortIfStatementsOnASingleLine: WithoutElse +AllowShortLoopsOnASingleLine: true +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: MultiLine +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: Never + AfterEnum: false + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Linux +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 0 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DeriveLineEnding: true +DerivePointerAlignment: true +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^"(llvm|llvm-c|clang|clang-c)/' + Priority: 2 + SortPriority: 0 + - Regex: '^(<|"(gtest|gmock|isl|json)/)' + Priority: 3 + SortPriority: 0 + - Regex: '.*' + Priority: 1 + SortPriority: 0 +IncludeIsMainRegex: '(Test)?$' +IncludeIsMainSourceRegex: '' +IndentCaseLabels: true +IndentCaseBlocks: false +IndentGotoLabels: true +IndentPPDirectives: None +IndentExternBlock: AfterExternBlock +IndentWidth: 4 +IndentWrappedFunctionNames: false +InsertTrailingCommas: None +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: true +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Auto +ObjCBlockIndentWidth: 2 +ObjCBreakBeforeNestedBlockParam: true +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 60 +PointerAlignment: Right +ReflowComments: true +SortIncludes: false +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInConditionalStatement: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +SpaceBeforeSquareBrackets: false +Standard: Latest +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION +TabWidth: 8 +UseCRLF: false +UseTab: Never +WhitespaceSensitiveMacros: + - STRINGIZE + - PP_STRINGIZE + - BOOST_PP_STRINGIZE +... + diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4b1fd7d..4993a56 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,14 +4,14 @@ name: Build Test # events but only for the main branch on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: build_and_test: - name: '${{ matrix.os }}: build and test' + name: "${{ matrix.os }}: build and test" runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -29,24 +29,23 @@ jobs: - name: get-cmake uses: lukka/get-cmake@latest - - name: run-cmake - uses: lukka/run-cmake@v3.0 - with: - cmakeGenerator: 'Ninja' - cmakeListsOrSettingsJson: 'CMakeListsTxtBasic' - buildDirectory: '${{ runner.workspace }}/build/ninja/' + - name: Build project + working-directory: "${{ runner.workspace }}/" + run: | + cmake -B ./build -S ./cmake-rust-demo + cmake --build ./build - name: Run tests - working-directory: '${{ runner.workspace }}/build/ninja/' + working-directory: "${{ runner.workspace }}/build/" run: | ctest -VV - name: Run the app run: | - ${{ runner.workspace }}/build/ninja/app/app + ${{ runner.workspace }}/build/app/app build_and_test_windows: - name: 'windows-latest: build and test' + name: "windows-latest: build and test" runs-on: windows-latest steps: @@ -60,18 +59,17 @@ jobs: - name: get-cmake uses: lukka/get-cmake@latest - - name: run-cmake - uses: lukka/run-cmake@v3.0 - with: - cmakeGenerator: 'VS16Win64' - cmakeListsOrSettingsJson: 'CMakeListsTxtBasic' - buildDirectory: '${{ runner.workspace }}/build/vs2019/' + - name: Build project + working-directory: "${{ runner.workspace }}/" + run: | + cmake -B ./build -S ./cmake-rust-demo -A x64 + cmake --build ./build - name: Run tests - working-directory: '${{ runner.workspace }}/build/vs2019/' + working-directory: "${{ runner.workspace }}/build/" run: | ctest -VV -C Debug - name: Run the app run: | - ${{ runner.workspace }}\\build\\vs2019\\app\\Debug\\app.exe + ${{ runner.workspace }}\\build\\app\\Debug\\app.exe diff --git a/.gitignore b/.gitignore index f3afb0e..5761881 100644 --- a/.gitignore +++ b/.gitignore @@ -88,3 +88,4 @@ build/* # Rust / Cargo files Cargo.lock +target/* diff --git a/CMakeLists.txt b/CMakeLists.txt index b9ac188..315bfc3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,10 +1,11 @@ -# Copyright (C) 2020 Micah Snyder. +# Copyright (C) 2020-2022 Micah Snyder. cmake_minimum_required(VERSION 3.18) +set(RUSTC_MINIMUM_REQUIRED 1.56) -project( RustCMakeDemo - VERSION "0.1.0" - DESCRIPTION "A demo app to show a CMake project with components written in Rust." ) +project(RustCMakeDemo + VERSION "0.2.0" + DESCRIPTION "A demo app to show a CMake project with components written in Rust.") set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH}) @@ -16,14 +17,14 @@ option(MAINTAINER_MODE "Use `cbindgen` to generate Rust library API headers." ${MAINTAINER_MODE_DEFAULT}) -if(MAINTAINER_MODE) - set(cbindgen_REQUIRED 1) -endif() find_package(Rust REQUIRED) # Always use '-fPIC'/'-fPIE' option. set(CMAKE_POSITION_INDEPENDENT_CODE ON) +# Include GNUInstallDirs for access to CMAKE_INSTALL_LIBDIR, etc +include(GNUInstallDirs) + # Enable CTest if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) include(CTest) @@ -44,9 +45,10 @@ include(CPack) # # Build targets. # -add_subdirectory( lib ) -add_subdirectory( common ) -add_subdirectory( app ) +add_subdirectory(lib) +add_subdirectory(common) +add_subdirectory(app) +add_subdirectory(test) # # The Summary Info. @@ -58,10 +60,7 @@ message(STATUS "Configuration Options Summary -- Build type: ${CMAKE_BUILD_TYPE} C compiler: ${CMAKE_C_COMPILER} Rust toolchain: ${cargo_EXECUTABLE} (${cargo_VERSION}) + ${rustc_EXECUTABLE} (${rustc_VERSION}) CFLAGS: ${CMAKE_C_FLAGS_${_build_type}} ${CMAKE_C_FLAGS} Build Options: Maintainer Mode: ${MAINTAINER_MODE}") -if(MAINTAINER_MODE) -message("\ - cbindgen: ${cbindgen_EXECUTABLE} (${cbindgen_VERSION})") -endif() diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0943149 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,6 @@ +[workspace] + +members = ["lib/rust", "common/gen_uuid"] + +[profile.dev.package."*"] +opt-level = 2 diff --git a/README.md b/README.md index 32f3924..13280ce 100644 --- a/README.md +++ b/README.md @@ -10,27 +10,24 @@ The notable feature of this project is [cmake/FindRust.cmake](cmake/FindRust.cma Add `FindRust.cmake` to your project's `cmake` directory and use the following to enable Rust support: ```cmake -if(MAINTAINER_MODE) - set(cbindgen_REQUIRED 1) -endif() find_package(Rust REQUIRED) ``` To build a rust library and link it into your app, use: ```cmake -add_rust_library( TARGET yourlib WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/yourlib" ) +add_rust_library(TARGET yourlib WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/yourlib") -add_executable( yourexe ) -target_sources( yourexe PRIVATE yourexe.c ) -target_link_libraries( yourexe yourlib ) +add_executable(yourexe) +target_sources(yourexe PRIVATE yourexe.c) +target_link_libraries(yourexe yourlib) ``` For unit test support, you can use the `add_rust_test()` function, like this: ```cmake -add_rust_library( TARGET yourlib WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/yourlib" ) -add_rust_test( NAME yourlib WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/yourlib" ) +add_rust_library(TARGET yourlib WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/yourlib") +add_rust_test(NAME yourlib WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/yourlib") ``` And don't forget to enable CTest early in your top-level `CMakeLists.txt` file: @@ -43,9 +40,19 @@ if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) endif() ``` -## `cbindgen` C API generation support +## Minimum Rust version + +You may set the CMake variable `RUSTC_MINIMUM_REQUIRED` to enforce a minimum Rust version, such as "1.56" for the 2021 edition support. + +## `cbindgen` and `bindgen` FFI generation + +This project demonstrates using `bindgen` and `cbindgen` within the `/lib/rust/build.rs` script to generate the API's at build time. + +`cbindgen` is used to generate a `demorust.h` C binding for the Rust exports every time you build. It'll be dropped in the build directory. + +`bindgen`, on the otherhand, generates a `sys.rs` Rust binding for the C exports required by the Rust code. Unfortunately, `bindgen` depends on libclang for some features that aren't readily available on all systems. So we only run `bindgen` if you set the `MAINTAINER_MODE` CMake parameter to `ON`. That works out okay, as the `sys.rs` file is dropped into the source directory, not the build directory. But that means you have to remember to build with `MAINTAINER_MODE=ON` any time you change the internal C API's used by the Rust library. -If you set `cbindgen_REQUIRED` as shown above, then `cbindgen` will need to be installed. It will also require a `cbindgen.toml` file next to each `Cargo.toml`. +The `cbindgen` and `bindgen` programs don't need to be pre-installed. `Cargo.toml` will pull them in during the build. ## Building this project @@ -56,7 +63,12 @@ Requirements: Run: ```bash mkdir build && cd build -cmake .. && cmake --build . && ctest +cmake .. \ + -D MAINTAINER_MODE=ON \ + -D CMAKE_INSTALL_PREFIX=install +cmake --build . +ctest -V +cmake --build . --target install ``` ## Vendoring dependencies @@ -92,6 +104,38 @@ You'll note that the vendored dependencies appear in your source directory. To r rm -rf **/.cargo ``` +# Installing Rust binaries with CMake + +C static libraries don't link in other static libraries. Fully-static builds of projects that are composed of a few different static libraries will result in a collection of static libraries. If you're only providing an application, then you can link them into the app and only install the app. But if you provide a library for others to consume then you may need to install all of the static libraries that compose the features of "the library" you provide for downstream projects. + +For example, let's say you've got a C library that may be built as a shared lib AND as a static lib, and you're slowly porting the C code into a Rust static library. You will link your Rust static library into the C library to maintain the original C API for your downstream users. With a shared-build of the C library, the Rust static library will get linked into the shared library and the downstream users never have to know it exists. Buf for a static-build of the C library, the C library is linked *against* your Rust library but they remain two separate libs that must both be linked with the downstream applications. You will need to install both the C static library *and* the Rust static library. + +Projects built with CMake can use CMake to install the software directly (e.g. under `/usr/local`) or via a packager like WiX Toolset (Windows) or `.deb` / `.rpm` / `.pkg` packages. + +Rust binaries aren't treated quite the same by CMake as native C binaries, but you can use CMake to install them. + +## How to install Rust binaries with CMake + +The first thing you'll probably need if you want to use CMake to install stuff, whether or not you bundle in some Rust binaries, is to include the GNUEInstallDirs module somewhere at the top of your top-level `CMakeLists.txt`: + +```c +include(GNUInstallDirs) +``` + +Now with a regular C library or executable CMake target, you might configure them for installation like this: +```c +install(TARGETS demo DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT libraries) +``` +or: +```c +install(TARGETS app DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT programs) +``` + +Rust library CMake targets aren't normal CMake binary targets though. They're "custom" targets, which means you will instead have to use `install(FILES` instead of `install(TARGETS`, and then point CMake at the specific file you need installed instead of at a target. Our `FindRust.cmake`'s `add_rust_library()` function makes this easy. WHen you add a Rust library, it sets the target properties such that you can simply use CMake's `$` [generator expression](https://cmake.org/cmake/help/latest/manual/cmake-generator-expressions.7.html) to provide the file path. In this demo, we configure installation for our `demorust` Rust static library like this: +```c +install(FILES $ DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT libraries) +``` + ## License This project is dual-licensed under MIT and Apache 2.0. diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 7282a4e..ee0a4db 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -6,14 +6,16 @@ # # The app. -add_executable( app ) -target_sources( app PRIVATE app.c ) -target_link_libraries( app +add_executable(app) +target_sources(app PRIVATE app.c) +target_link_libraries(app PRIVATE - CMakeRust::lib - CMakeRust::gen_uuid ) + demo::rust_static_lib + demo::static_lib + demo::gen_uuid) if(WIN32) - target_link_libraries( app + target_link_libraries(app PRIVATE - Bcrypt ) + Bcrypt) endif() +install(TARGETS app DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT programs) diff --git a/app/app.c b/app/app.c index 73c124e..816855b 100644 --- a/app/app.c +++ b/app/app.c @@ -8,10 +8,11 @@ #include #include -#include "cmakerust.h" +#include "demo-rust.h" #include "gen_uuid.h" -int main(void) { +int main(void) +{ char *my_uuid = {0}; my_uuid = gen_uuid(); diff --git a/cmake/FindRust.cmake b/cmake/FindRust.cmake index ccf47ec..6b122eb 100644 --- a/cmake/FindRust.cmake +++ b/cmake/FindRust.cmake @@ -1,7 +1,7 @@ # Find the Rust toolchain and add the `add_rust_library()` API to build Rust # libraries. # -# Copyright (C) 2020-2021 Micah Snyder. +# Copyright (C) 2020-2022 Micah Snyder. # # Author: Micah Snyder # To see this in a sample project, visit: https://github.com/micahsnyder/cmake-rust-demo @@ -23,37 +23,42 @@ # - rustdoc # - rustfmt # - bindgen -# - cbindgen -# -# Note that `cbindgen` is presently 3rd-party, and is not included with the -# standard Rust installation. `bindgen` is a part of the rust toolchain, but -# might need to be installed separately. # # Callers can make any program mandatory by setting `_REQUIRED` before # the call to `find_package(Rust)` # # Eg: -# -# if(MAINTAINER_MODE) -# set(cbindgen_REQUIRED 1) -# set(bindgen_REQUIRED 1) -# endif() # find_package(Rust REQUIRED) # -# This module also provides an `add_rust_library()` function which allows a -# caller to create a Rust static library target which you can link to with -# `target_link_libraries()`. +# This module also provides: +# +# - `add_rust_library()` - This allows a caller to create a Rust static library +# target which you can link to with `target_link_libraries()`. +# +# Your Rust static library target will itself depend on the native static libs +# you get from `rustc --crate-type staticlib --print=native-static-libs /dev/null` +# +# The CARGO_CMD environment variable will be set to "BUILD" so you can tell +# it's not building the unit tests inside your (optional) `build.rs` file. +# +# Example `add_rust_library()` usage: +# +# add_rust_library(TARGET yourlib WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}") +# add_library(YourProject::yourlib ALIAS yourlib) # -# Your Rust static library target will itself depend on the native static libs -# you get from `rustc --crate-type staticlib --print=native-static-libs /dev/null` +# add_executable(yourexe) +# target_link_libraries(yourexe YourProject::yourlib) # -# Example `add_rust_library()` usage: +# - `add_rust_test()` - This allows a caller to run `cargo test` for a specific +# Rust target as a CTest test. # -# add_rust_library(TARGET yourlib WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}") -# add_library(YourProject::yourlib ALIAS yourlib) +# The CARGO_CMD environment variable will be set to "TEST" so you can tell +# it's not building the unit tests inside your (optional) `build.rs` file. # -# add_executable(yourexe) -# target_link_libraries(yourexe YourProject::yourlib) +# Example `add_rust_library()` usage: +# +# add_rust_test(NAME yourlib WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/yourlib") +# set_property(TEST yourlib PROPERTY ENVIRONMENT ${ENVIRONMENT}) # if(NOT DEFINED CARGO_HOME) @@ -141,9 +146,9 @@ function(add_rust_library) cmake_parse_arguments(ARGS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) if(WIN32) - set(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${LIB_TARGET}/${LIB_BUILD_TYPE}/${ARGS_TARGET}.lib") + set(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${RUST_COMPILER_TARGET}/${LIB_BUILD_TYPE}/${ARGS_TARGET}.lib") else() - set(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${LIB_TARGET}/${LIB_BUILD_TYPE}/lib${ARGS_TARGET}.a") + set(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${RUST_COMPILER_TARGET}/${LIB_BUILD_TYPE}/lib${ARGS_TARGET}.a") endif() file(GLOB_RECURSE LIB_SOURCES "${ARGS_WORKING_DIRECTORY}/*.rs") @@ -152,22 +157,24 @@ function(add_rust_library) list(APPEND MY_CARGO_ARGS "--target-dir" ${CMAKE_CURRENT_BINARY_DIR}) list(JOIN MY_CARGO_ARGS " " MY_CARGO_ARGS_STRING) - # Build the library and generate the c-binding, if `cbindgen` is required. - if(${cbindgen_REQUIRED}) + # Build the library and generate the c-binding + if("${CMAKE_OSX_ARCHITECTURES}" MATCHES "^arm64;x86_64$") add_custom_command( OUTPUT "${OUTPUT}" - COMMAND ${CMAKE_COMMAND} -E env "CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR}" ${cargo_EXECUTABLE} ARGS ${MY_CARGO_ARGS} - COMMAND ${cbindgen_EXECUTABLE} --lang c -o ${ARGS_WORKING_DIRECTORY}/${ARGS_TARGET}.h ${ARGS_WORKING_DIRECTORY} + COMMAND ${CMAKE_COMMAND} -E env "CARGO_CMD=build" "CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR}" "MAINTAINER_MODE=${MAINTAINER_MODE}" "RUSTFLAGS=\"${RUSTFLAGS}\"" ${cargo_EXECUTABLE} ARGS ${MY_CARGO_ARGS} --target=x86_64-apple-darwin + COMMAND ${CMAKE_COMMAND} -E env "CARGO_CMD=build" "CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR}" "MAINTAINER_MODE=${MAINTAINER_MODE}" "RUSTFLAGS=\"${RUSTFLAGS}\"" ${cargo_EXECUTABLE} ARGS ${MY_CARGO_ARGS} --target=aarch64-apple-darwin + COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/${RUST_COMPILER_TARGET}/${LIB_BUILD_TYPE}" + COMMAND lipo ARGS -create ${CMAKE_CURRENT_BINARY_DIR}/x86_64-apple-darwin/${LIB_BUILD_TYPE}/lib${ARGS_TARGET}.a ${CMAKE_CURRENT_BINARY_DIR}/aarch64-apple-darwin/${LIB_BUILD_TYPE}/lib${ARGS_TARGET}.a -output "${OUTPUT}" WORKING_DIRECTORY "${ARGS_WORKING_DIRECTORY}" DEPENDS ${LIB_SOURCES} - COMMENT "Building ${ARGS_TARGET} in ${ARGS_WORKING_DIRECTORY} with:\n\t ${cargo_EXECUTABLE} ${MY_CARGO_ARGS_STRING}") + COMMENT "Building ${ARGS_TARGET} in ${ARGS_WORKING_DIRECTORY} with: ${cargo_EXECUTABLE} ${MY_CARGO_ARGS_STRING}") else() add_custom_command( OUTPUT "${OUTPUT}" - COMMAND ${CMAKE_COMMAND} -E env "CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR}" ${cargo_EXECUTABLE} ARGS ${MY_CARGO_ARGS} + COMMAND ${CMAKE_COMMAND} -E env "CARGO_CMD=build" "CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR}" "MAINTAINER_MODE=${MAINTAINER_MODE}" "RUSTFLAGS=\"${RUSTFLAGS}\"" ${cargo_EXECUTABLE} ARGS ${MY_CARGO_ARGS} WORKING_DIRECTORY "${ARGS_WORKING_DIRECTORY}" DEPENDS ${LIB_SOURCES} - COMMENT "Building ${ARGS_TARGET} in ${ARGS_WORKING_DIRECTORY} with:\n\t ${cargo_EXECUTABLE} ${MY_CARGO_ARGS_STRING}") + COMMENT "Building ${ARGS_TARGET} in ${ARGS_WORKING_DIRECTORY} with: ${cargo_EXECUTABLE} ${MY_CARGO_ARGS_STRING}") endif() # Create a target from the build output @@ -183,7 +190,7 @@ function(add_rust_library) set_target_properties(${ARGS_TARGET} PROPERTIES IMPORTED_LOCATION "${OUTPUT}" - INTERFACE_INCLUDE_DIRECTORIES "${ARGS_WORKING_DIRECTORY}" + INTERFACE_INCLUDE_DIRECTORIES "${ARGS_WORKING_DIRECTORY};${CMAKE_CURRENT_BINARY_DIR}" ) # Vendor the dependencies, if desired @@ -197,9 +204,21 @@ function(add_rust_test) set(oneValueArgs NAME WORKING_DIRECTORY) cmake_parse_arguments(ARGS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + set(MY_CARGO_ARGS "test") + if(NOT "${CMAKE_OSX_ARCHITECTURES}" MATCHES "^arm64;x86_64$") # Don't specify the target for universal, we'll do that manually for each build. + list(APPEND MY_CARGO_ARGS "--target" ${RUST_COMPILER_TARGET}) + endif() + + if("${CMAKE_BUILD_TYPE}" STREQUAL "Release") + list(APPEND MY_CARGO_ARGS "--release") + endif() + + list(APPEND MY_CARGO_ARGS "--target-dir" ${CMAKE_CURRENT_BINARY_DIR}) + list(JOIN MY_CARGO_ARGS " " MY_CARGO_ARGS_STRING) + add_test( - NAME test-${ARGS_NAME} - COMMAND ${CMAKE_COMMAND} -E env "CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR}" ${cargo_EXECUTABLE} test -vv --color always + NAME ${ARGS_NAME} + COMMAND ${CMAKE_COMMAND} -E env "CARGO_CMD=test" "CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR}" ${cargo_EXECUTABLE} ${MY_CARGO_ARGS} --color always WORKING_DIRECTORY ${ARGS_WORKING_DIRECTORY} ) endfunction() @@ -218,7 +237,11 @@ find_rust_program(rust-lldb) find_rust_program(rustdoc) find_rust_program(rustfmt) find_rust_program(bindgen) -find_rust_program(cbindgen) + +if(RUSTC_MINIMUM_REQUIRED AND rustc_VERSION VERSION_LESS RUSTC_MINIMUM_REQUIRED) + message(FATAL_ERROR "Your Rust toolchain is to old to build this project: + ${rustc_VERSION} < ${RUSTC_MINIMUM_REQUIRED}") +endif() # Determine the native libs required to link w/ rust static libs # message(STATUS "Detecting native static libs for rust: ${rustc_EXECUTABLE} --crate-type staticlib --print=native-static-libs /dev/null") @@ -245,51 +268,52 @@ foreach(LINE ${LINE_LIST}) endif() endforeach() -if(WIN32) - if(CMAKE_SIZEOF_VOID_P EQUAL 8) - set(LIB_TARGET "x86_64-pc-windows-msvc") - else() - set(LIB_TARGET "i686-pc-windows-msvc") - endif() -elseif(ANDROID) - if(ANDROID_SYSROOT_ABI STREQUAL "x86") - set(LIB_TARGET "i686-linux-android") - elseif(ANDROID_SYSROOT_ABI STREQUAL "x86_64") - set(LIB_TARGET "x86_64-linux-android") - elseif(ANDROID_SYSROOT_ABI STREQUAL "arm") - set(LIB_TARGET "arm-linux-androideabi") - elseif(ANDROID_SYSROOT_ABI STREQUAL "arm64") - set(LIB_TARGET "aarch64-linux-android") - endif() -elseif(IOS) - set(LIB_TARGET "universal") -elseif(CMAKE_SYSTEM_NAME STREQUAL Darwin) - set(LIB_TARGET "x86_64-apple-darwin") -else() - if(CMAKE_SIZEOF_VOID_P EQUAL 8) - set(LIB_TARGET "x86_64-unknown-linux-gnu") +if(NOT RUST_COMPILER_TARGET) + # Automatically determine the Rust Target Triple. + # Note: Users may override automatic target detection by specifying their own. Most likely needed for cross-compiling. + # For reference determining target platform: https://doc.rust-lang.org/nightly/rustc/platform-support.html + if(WIN32) + # For windows x86/x64, it's easy enough to guess the target. + if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(RUST_COMPILER_TARGET "x86_64-pc-windows-msvc") + else() + set(RUST_COMPILER_TARGET "i686-pc-windows-msvc") + endif() + elseif(CMAKE_SYSTEM_NAME STREQUAL Darwin AND "${CMAKE_OSX_ARCHITECTURES}" MATCHES "^arm64;x86_64$") + # Special case for Darwin because we may want to build universal binaries. + set(RUST_COMPILER_TARGET "universal-apple-darwin") else() - set(LIB_TARGET "i686-unknown-linux-gnu") + # Determine default LLVM target triple. + execute_process(COMMAND ${rustc_EXECUTABLE} -vV + OUTPUT_VARIABLE RUSTC_VV_OUT ERROR_QUIET) + string(REGEX REPLACE "^.*host: ([a-zA-Z0-9_\\-]+).*" "\\1" DEFAULT_RUST_COMPILER_TARGET1 "${RUSTC_VV_OUT}") + string(STRIP ${DEFAULT_RUST_COMPILER_TARGET1} DEFAULT_RUST_COMPILER_TARGET) + + set(RUST_COMPILER_TARGET "${DEFAULT_RUST_COMPILER_TARGET}") endif() endif() -if(IOS) - set(CARGO_ARGS "lipo") -else() - set(CARGO_ARGS "build") - list(APPEND CARGO_ARGS "--target" ${LIB_TARGET}) +set(CARGO_ARGS "build") +if(NOT "${RUST_COMPILER_TARGET}" MATCHES "^universal-apple-darwin$") + # Don't specify the target for macOS universal builds, we'll do that manually for each build. + list(APPEND CARGO_ARGS "--target" ${RUST_COMPILER_TARGET}) endif() +set(RUSTFLAGS "") if(NOT CMAKE_BUILD_TYPE) set(LIB_BUILD_TYPE "debug") -elseif(${CMAKE_BUILD_TYPE} STREQUAL "Release") +elseif(${CMAKE_BUILD_TYPE} STREQUAL "Release" OR ${CMAKE_BUILD_TYPE} STREQUAL "MinSizeRel") + set(LIB_BUILD_TYPE "release") + list(APPEND CARGO_ARGS "--release") +elseif(${CMAKE_BUILD_TYPE} STREQUAL "RelWithDebInfo") set(LIB_BUILD_TYPE "release") list(APPEND CARGO_ARGS "--release") + set(RUSTFLAGS "-g") else() set(LIB_BUILD_TYPE "debug") endif() -find_package_handle_standard_args( Rust +find_package_handle_standard_args(Rust REQUIRED_VARS cargo_EXECUTABLE VERSION_VAR cargo_VERSION ) diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index d272a2e..e600729 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -1,10 +1,14 @@ -# Copyright (C) 2020 Micah Snyder. +# Copyright (C) 2020-2022 Micah Snyder. # -# Rust common libraries +# Libraries that may be used by the applications. # # A library to generate UUID's add_rust_library(TARGET gen_uuid WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/gen_uuid") + +# The unit tests for this module have no dependencies on C libraries or other special +# test environment considerations, so we may as well add the test right here instead of +# adding it in the `test/CMakeLists.txt` file. add_rust_test(NAME gen_uuid WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/gen_uuid") -add_library(CMakeRust::gen_uuid ALIAS gen_uuid) +add_library(demo::gen_uuid ALIAS gen_uuid) diff --git a/common/gen_uuid/Cargo.toml b/common/gen_uuid/Cargo.toml index cbedfd0..9e56e6a 100644 --- a/common/gen_uuid/Cargo.toml +++ b/common/gen_uuid/Cargo.toml @@ -1,15 +1,8 @@ [package] +authors = ["Micah Snyder"] +edition = "2021" name = "gen_uuid" -version = "0.1.0" -authors = ["Micah Snyder "] -edition = "2018" - -[lib] -name = "gen_uuid" -path = "gen_uuid.rs" -crate-type = ["staticlib"] - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +version = "0.2.0" [dependencies] libc = "0.2.77" @@ -17,3 +10,10 @@ libc = "0.2.77" [dependencies.uuid] version = "0.8.1" features = ["v4"] + +[lib] +crate-type = ["staticlib"] +name = "gen_uuid" + +[build-dependencies] +cbindgen = "0.20" diff --git a/common/gen_uuid/build.rs b/common/gen_uuid/build.rs new file mode 100644 index 0000000..d28dd59 --- /dev/null +++ b/common/gen_uuid/build.rs @@ -0,0 +1,51 @@ +use std::env; +use std::path::PathBuf; + +const C_HEADER_OUTPUT: &str = "gen_uuid.h"; + +// Environment variable name prefixes worth including for diags +const ENV_PATTERNS: &[&str] = &["CARGO_", "RUST", "LIB"]; + +fn main() -> Result<(), &'static str> { + eprintln!("build.rs command line: {:?}", std::env::args()); + eprintln!("Environment:"); + std::env::vars() + .filter(|(k, _)| ENV_PATTERNS.iter().any(|prefix| k.starts_with(prefix))) + .for_each(|(k, v)| eprintln!(" {}={:?}", k, v)); + + // We only want to generate bindings for `cargo build`, not `cargo test`. + // FindRust.cmake defines $CARGO_CMD so we can differentiate. + let cargo_cmd = env::var("CARGO_CMD").unwrap_or_else(|_| "".into()); + + match cargo_cmd.as_str() { + "build" => { + // Generate bindings as a part of the build. + + // Always generate the C-headers when CMake kicks off a build. + generate_c_bindings()?; + + // No rust bindings needed. This module doesn't call into our C libs. + } + + _ => { + return Ok(()); + } + } + + Ok(()) +} + +/// Use cbindgen to generate C-headers for Rust library. +fn generate_c_bindings() -> Result<(), &'static str> { + let crate_dir = env::var("CARGO_MANIFEST_DIR").or(Err("CARGO_MANIFEST_DIR not specified"))?; + let build_dir = PathBuf::from(env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| ".".into())); + let outfile_path = build_dir.join(C_HEADER_OUTPUT); + + // Useful for build diagnostics + eprintln!("cbindgen outputting {:?}", &outfile_path); + cbindgen::generate(crate_dir) + .expect("Unable to generate C headers for Rust code") + .write_to_file(&outfile_path); + + Ok(()) +} diff --git a/common/gen_uuid/gen_uuid.h b/common/gen_uuid/gen_uuid.h index 1323bde..a0def05 100644 --- a/common/gen_uuid/gen_uuid.h +++ b/common/gen_uuid/gen_uuid.h @@ -10,7 +10,6 @@ #include #include - void free_uuid(char *uuid_ptr); char *gen_uuid(void); diff --git a/common/gen_uuid/gen_uuid.rs b/common/gen_uuid/gen_uuid.rs deleted file mode 100644 index d000076..0000000 --- a/common/gen_uuid/gen_uuid.rs +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Example static lib for use in project apps - * - * Copyright (C) 2020 Micah Snyder. - */ - -use libc::c_char; -use std::ffi::CString; - -use uuid::Uuid; - -#[no_mangle] -pub extern "C" fn gen_uuid() -> *mut c_char { - let uuid_str = Uuid::new_v4().to_string(); - - let c_uuid = CString::new(uuid_str).unwrap(); - c_uuid.into_raw() -} - -#[no_mangle] -pub extern "C" fn free_uuid(uuid_ptr: *mut c_char ) -> () { - unsafe { - if uuid_ptr.is_null() { - return; - } - CString::from_raw(uuid_ptr) - }; -} - -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); - } -} diff --git a/common/gen_uuid/src/lib.rs b/common/gen_uuid/src/lib.rs new file mode 100644 index 0000000..e9bf59f --- /dev/null +++ b/common/gen_uuid/src/lib.rs @@ -0,0 +1,41 @@ +/* + * Example static lib for use in project apps + * + * Copyright (C) 2020-2022 Micah Snyder. + */ + +use libc::c_char; +use std::ffi::CString; + +use uuid::Uuid; + +/// Generate / allocate a UUID structure +#[no_mangle] +pub extern "C" fn gen_uuid() -> *mut c_char { + let uuid_str = Uuid::new_v4().to_string(); + + let c_uuid = CString::new(uuid_str).unwrap(); + c_uuid.into_raw() +} + +/// Free a UUID structure +/// +/// # Safety +/// +/// uuid_ptr must be a valid pointer. +#[no_mangle] +pub unsafe extern "C" fn free_uuid(uuid_ptr: *mut c_char) { + if uuid_ptr.is_null() { + return; + } + let _ = CString::from_raw(uuid_ptr); +} + +#[cfg(test)] +mod tests { + /// faux test to demonstrate running rust unit tests through CMake / CTest + #[test] + fn test_gen_uuid() { + assert_eq!(2 + 2, 4); + } +} diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index f049500..eb2382a 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -1,47 +1,21 @@ -# Copyright (C) 2020 Micah Snyder. - -configure_file(cmakerust-version.h.in cmakerust-version.h) +# Copyright (C) 2020-2022 Micah Snyder. # -# Features written in Rust +# The demo Rust library (features ported from the "original" C library to Rust) # -# Note: These will be compiled to static library targets and must be added as -# dependencies to both the C object targets and shared/static library targets. +# Note: The rust port will be compiled to static library target and must be added as +# a dependency to both the C object targets and shared/static library targets. # +add_rust_library(TARGET demorust WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/rust") +install(FILES $ DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT libraries) +add_library(demo::rust_static_lib ALIAS demorust) -# Colorful Logging -add_rust_library(TARGET colorlog WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/colorlog") -add_rust_test(NAME colorlog WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/colorlog") - -configure_file(cmakerust-version.h.in cmakerust-version.h) +# The unit tests for the demorust module dependends on the C library and may even need +# to be linked with the C library's 3rd party library dependencies. So to be able to build +# that application, we'll need to pass these things through the environment. +# Thus, we'll set all of that up and add the Rust-test in `tests/CMakelists.txt`. # -# The cmakerust C library +# The demo C library # -add_library( cmakerust_obj OBJECT ) -target_sources( cmakerust_obj - PRIVATE lib.c lib_private.h - PUBLIC cmakerust.h ) -target_link_libraries( cmakerust_obj PUBLIC colorlog ) -if(WIN32) - target_link_libraries( cmakerust_obj PUBLIC Userenv wsock32 ws2_32 ) -endif() -target_include_directories( cmakerust_obj PUBLIC ${CMAKE_CURRENT_BINARY_DIR} ) - -# The cmakerust shared library. -add_library( cmakerust SHARED ) -target_sources( cmakerust PUBLIC cmakerust.h ) -target_link_libraries( cmakerust PUBLIC cmakerust_obj ) -target_include_directories( cmakerust PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} ) -if(WIN32) - set_target_properties( cmakerust PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON) -endif() -add_library( CMakeRust::lib ALIAS cmakerust ) - -# The cmakerust static library. -add_library( cmakerust_static STATIC ) -target_sources( cmakerust_static PUBLIC cmakerust.h ) -target_link_libraries( cmakerust_static PUBLIC cmakerust_obj ) -set_target_properties( cmakerust_static PROPERTIES ARCHIVE_OUTPUT_NAME cmakerust_static ) -target_include_directories( cmakerust PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} ) -add_library( CMakeRust::static_lib ALIAS cmakerust_static ) +add_subdirectory(c) diff --git a/lib/c/CMakeLists.txt b/lib/c/CMakeLists.txt new file mode 100644 index 0000000..9cc81ca --- /dev/null +++ b/lib/c/CMakeLists.txt @@ -0,0 +1,41 @@ +# Copyright (C) 2020-2022 Micah Snyder. + +configure_file(demo-version.h.in demo-version.h) + +# +# The demo C library +# +add_library(demo_obj OBJECT) +target_sources(demo_obj + PRIVATE + demo.c demo-private.h + dostuff.c dostuff.h + PUBLIC + demo.h +) +target_link_libraries(demo_obj PUBLIC demorust) +if(WIN32) + target_link_libraries(demo_obj PUBLIC Userenv wsock32 ws2_32) +endif() +target_include_directories(demo_obj PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) + +# The demo shared library. +add_library(demo SHARED) +target_sources(demo PUBLIC demo.h) +target_link_libraries(demo PUBLIC demo_obj) +target_include_directories(demo PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) +if(WIN32) + set_target_properties(demo PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON) + target_link_libraries(demo PRIVATE Bcrypt) +endif() +install(TARGETS demo DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT libraries) +add_library(demo::lib ALIAS demo) + +# The demo static library. +add_library(demo_static STATIC) +target_sources(demo_static PUBLIC demo.h) +target_link_libraries(demo_static PUBLIC demo_obj) +target_include_directories(demo PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) +set_target_properties(demo_static PROPERTIES ARCHIVE_OUTPUT_NAME demo_static) +install(TARGETS demo_static DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT libraries) +add_library(demo::static_lib ALIAS demo_static) diff --git a/lib/lib_private.h b/lib/c/demo-private.h similarity index 100% rename from lib/lib_private.h rename to lib/c/demo-private.h diff --git a/lib/cmakerust-version.h.in b/lib/c/demo-version.h.in similarity index 100% rename from lib/cmakerust-version.h.in rename to lib/c/demo-version.h.in diff --git a/lib/lib.c b/lib/c/demo.c similarity index 61% rename from lib/lib.c rename to lib/c/demo.c index 6f9b7dd..9194a73 100644 --- a/lib/lib.c +++ b/lib/c/demo.c @@ -1,20 +1,22 @@ /* * Example library that has some features written in Rust. * - * Copyright (C) 2020 Micah Snyder. + * Copyright (C) 2020-2022 Micah Snyder. */ #include #include -#include "lib_private.h" -#include "cmakerust.h" -#include "colorlog.h" +#include "demo-private.h" +#include "demo.h" +#include "demo-rust.h" -void cmakerust_init() { +void cmakerust_init() +{ clog_debug((const uint8_t *)init_message, strlen(init_message)); } -void cmakerust_fini() { +void cmakerust_fini() +{ clog_debug((const uint8_t *)fini_message, strlen(fini_message)); } diff --git a/lib/c/demo.h b/lib/c/demo.h new file mode 100644 index 0000000..766a3ff --- /dev/null +++ b/lib/c/demo.h @@ -0,0 +1,14 @@ +/* + * Copyright (C) 2020-2022 Micah Snyder. + */ + +#ifndef __DEMO_H +#define __DEMO_H + +#include "demo-version.h" + +void cmakerust_init(); + +void cmakerust_fini(); + +#endif /* __DEMO_H */ diff --git a/lib/c/dostuff.c b/lib/c/dostuff.c new file mode 100644 index 0000000..d789ed1 --- /dev/null +++ b/lib/c/dostuff.c @@ -0,0 +1,15 @@ +#include +#include +#include + +bool do_the_thing(uint8_t *inout, size_t inout_size) +{ + // manipulate the input + size_t i; + + for (i = 0; i < inout_size; i++) { + inout[i] *= 2; + } + + return true; +} diff --git a/lib/c/dostuff.h b/lib/c/dostuff.h new file mode 100644 index 0000000..398dcc1 --- /dev/null +++ b/lib/c/dostuff.h @@ -0,0 +1,5 @@ +#include +#include +#include + +bool do_the_thing(uint8_t *inout, size_t inout_size); diff --git a/lib/cmakerust.h b/lib/cmakerust.h deleted file mode 100644 index c40bf7d..0000000 --- a/lib/cmakerust.h +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright (C) 2020 Micah Snyder. - */ - -#ifndef __CMAKERUST_H -#define __CMAKERUST_H - -#include "cmakerust-version.h" -#include "colorlog.h" - -void cmakerust_init(); - -void cmakerust_fini(); - -#endif /* __CMAKERUST_H */ diff --git a/lib/colorlog/Cargo.toml b/lib/colorlog/Cargo.toml deleted file mode 100644 index e190bd2..0000000 --- a/lib/colorlog/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "colorlog" -version = "0.1.0" -authors = ["Micah Snyder "] -edition = "2018" - -[lib] -name = "colorlog" -path = "colorlog.rs" -crate-type = ["staticlib"] - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -colored = "2.0.0" diff --git a/lib/colorlog/colorlog.h b/lib/colorlog/colorlog.h deleted file mode 100644 index c287f4b..0000000 --- a/lib/colorlog/colorlog.h +++ /dev/null @@ -1,22 +0,0 @@ -/* Copyright (C) 2020 Micah Snyder. */ - -#ifndef __COLORLOG_H -#define __COLORLOG_H - -/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */ - -#include -#include -#include -#include - - -void clog_debug(const uint8_t *message, uintptr_t message_len); - -void clog_error(const uint8_t *message, uintptr_t message_len); - -void clog_info(const uint8_t *message, uintptr_t message_len); - -void clog_warning(const uint8_t *message, uintptr_t message_len); - -#endif /* __COLORLOG_H */ diff --git a/lib/rust/Cargo.toml b/lib/rust/Cargo.toml new file mode 100644 index 0000000..5dc5a28 --- /dev/null +++ b/lib/rust/Cargo.toml @@ -0,0 +1,16 @@ +[package] +authors = ["Micah Snyder"] +edition = "2021" +name = "demorust" +version = "0.2.0" + +[dependencies] +colored = "2.0.0" + +[lib] +crate-type = ["staticlib"] +name = "demorust" + +[build-dependencies] +cbindgen = "0.20" +bindgen = "0.59" diff --git a/lib/rust/build.rs b/lib/rust/build.rs new file mode 100644 index 0000000..697670d --- /dev/null +++ b/lib/rust/build.rs @@ -0,0 +1,215 @@ +use std::env; +use std::path::{Path, PathBuf}; + +use bindgen::builder; + +// A list of environment variables to query to determine additional libraries +// that need to be linked to resolve dependencies. +const LIB_ENV_LINK: &[&str] = &["LIBDEMO"]; + +// Additional [verbatim] libraries to link on Windows platforms +const LIB_LINK_WINDOWS: &[&str] = &["wsock32", "ws2_32", "Shell32", "User32"]; + +// Generate bindings for these functions: +const BINDGEN_FUNCTIONS: &[&str] = &["do_the_thing"]; + +// Generate bindings for these types (structs, enums): +const BINDGEN_TYPES: &[&str] = &[]; + +// Find the required functions and types in these headers: +const BINDGEN_HEADERS: &[&str] = &["../c/dostuff.h"]; + +// Find the required headers in these directories: +const BINDGEN_INCLUDE_PATHS: &[&str] = &["-I../c"]; + +// Write the bindings to this file: +const BINDGEN_OUTPUT_FILE: &str = "src/sys.rs"; + +const C_HEADER_OUTPUT: &str = "demo-rust.h"; + +// Environment variable name prefixes worth including for diags +const ENV_PATTERNS: &[&str] = &["CARGO_", "RUST", "LIB"]; + +fn main() -> Result<(), &'static str> { + eprintln!("build.rs command line: {:?}", std::env::args()); + eprintln!("Environment:"); + std::env::vars() + .filter(|(k, _)| ENV_PATTERNS.iter().any(|prefix| k.starts_with(prefix))) + .for_each(|(k, v)| eprintln!(" {}={:?}", k, v)); + + // We only want to generate bindings for `cargo build`, not `cargo test`. + // FindRust.cmake defines $CARGO_CMD so we can differentiate. + let cargo_cmd = env::var("CARGO_CMD").unwrap_or_else(|_| "".into()); + + match cargo_cmd.as_str() { + "build" => { + // Generate bindings as a part of the build. + + // Always generate the C-headers when CMake kicks off a build. + generate_c_bindings()?; + + let maintainer_mode = env::var("MAINTAINER_MODE").unwrap_or_else(|_| "".into()); + if maintainer_mode == "ON" { + // Only generate the `.rs` bindings when maintainer-mode is enabled. + // Bindgen requires libclang, which may not readily available, so we will commit the + // bindings to version control and use maintainer-mode to update them, as needed. + // On the plus-side, this means that our `.rs` file is present before our first build, + // so at least rust-analyzer will be happy. + generate_rust_bindings()?; + } + } + + "test" => { + // Link test executable with library dependencies. + println!("cargo:rerun-if-env-changed=LIBDEMO"); + + for var in LIB_ENV_LINK { + if !search_and_link_lib(var)? { + eprintln!("Undefined library dependency environment variable: {}", var); + return Err("Undefined library dependency environment variable"); + } + } + + if cfg!(windows) { + for lib in LIB_LINK_WINDOWS { + println!("cargo:rustc-link-lib={}", lib); + } + } + } + + _ => { + return Ok(()); + } + } + + Ok(()) +} + +/// Use bindgen to generate Rust bindings to call into C libraries. +fn generate_rust_bindings() -> Result<(), &'static str> { + let build_dir = PathBuf::from(env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| ".".into())); + let build_include_path = format!("-I{}", build_dir.join("..").to_str().unwrap()); + + // Configure and generate bindings. + let mut builder = builder() + // Silence code-style warnings for generated bindings. + .raw_line("#![allow(non_snake_case, non_camel_case_types, non_upper_case_globals)]") + // Make the bindings pretty. + .rustfmt_bindings(true) + // Disable the layout tests because we're committing `sys.rs` to source control. + // Pointer width, integer size, etc. are probably not the same when generated as when compiled. + .layout_tests(false) + // Enable bindgen to find generated headers in the build directory, too. + .clang_arg(build_include_path); + + for &include_path in BINDGEN_INCLUDE_PATHS { + builder = builder.clang_arg(include_path); + } + for &header in BINDGEN_HEADERS { + builder = builder.header(header); + } + for &c_function in BINDGEN_FUNCTIONS { + builder = builder.allowlist_function(c_function); + } + for &c_type in BINDGEN_TYPES { + builder = builder.allowlist_type(c_type); + } + + // Generate! + builder + .generate() + .expect("Unable to generate Rust bindings for C code") + .write_to_file(BINDGEN_OUTPUT_FILE) + .expect("Failed to write Rust bindings to output file"); + + Ok(()) +} + +/// Use cbindgen to generate C-headers for Rust library. +fn generate_c_bindings() -> Result<(), &'static str> { + let crate_dir = env::var("CARGO_MANIFEST_DIR").or(Err("CARGO_MANIFEST_DIR not specified"))?; + let build_dir = PathBuf::from(env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| ".".into())); + let outfile_path = build_dir.join(C_HEADER_OUTPUT); + + // Useful for build diagnostics + eprintln!("cbindgen outputting {:?}", &outfile_path); + cbindgen::generate(crate_dir) + .expect("Unable to generate C headers for Rust code") + .write_to_file(&outfile_path); + + Ok(()) +} + +/// Return whether the specified environment variable has been set, and output +/// linking directives as a side-effect +fn search_and_link_lib(environment_variable: &str) -> Result { + eprintln!(" - checking for {:?} in environment", environment_variable); + let filepath_str = match env::var(environment_variable) { + Err(env::VarError::NotPresent) => return Ok(false), + Err(env::VarError::NotUnicode(_)) => return Err("environment value not unicode"), + Ok(s) => { + if s.is_empty() { + return Ok(false); + } else { + s + } + } + }; + + let parsed_path = parse_lib_path(&filepath_str)?; + eprintln!( + " - adding {:?} to rustc library search path", + &parsed_path.dir + ); + println!("cargo:rustc-link-search={}", parsed_path.dir); + eprintln!(" - requesting that rustc link {:?}", &parsed_path.libname); + println!("cargo:rustc-link-lib={}", parsed_path.libname); + + Ok(true) +} + +/// Struct to store a lib name and directory. +/// Not the +struct ParsedLibraryPath { + dir: String, + libname: String, +} + +/// Parse a library path, returning: +/// - the directory containing the library +/// - the portion expected after the `-l` +fn parse_lib_path<'a>(path: &'a str) -> Result { + let path = PathBuf::from(path); + let file_name = path + .file_name() + .ok_or("file name not found")? + .to_str() + .ok_or("file name not unicode")?; + + // This can't fail because it came from a &str + let dir = path + .parent() + .unwrap_or_else(|| Path::new(".")) + .to_str() + .unwrap() + .to_owned(); + + // Grab the portion up to the first '.' + let full_libname = file_name + .split('.') + .next() + .ok_or("no '.' found in file name")?; + + let libname = if !cfg!(windows) { + // Trim off the "lib" for Linux/Unix systems + full_libname + .strip_prefix("lib") + .ok_or(r#"file name doesn't begin with "lib""#)? + } else { + // Keep the full libname on Windows. + full_libname + } + .to_owned(); + + Ok(ParsedLibraryPath { dir, libname }) +} diff --git a/lib/colorlog/cbindgen.toml b/lib/rust/cbindgen.toml similarity index 91% rename from lib/colorlog/cbindgen.toml rename to lib/rust/cbindgen.toml index 8f7ea7b..0cd2aac 100644 --- a/lib/colorlog/cbindgen.toml +++ b/lib/rust/cbindgen.toml @@ -8,8 +8,8 @@ language = "C" ############## Options for Wrapping the Contents of the Header ################# -header = "/* Copyright (C) 2020 Micah Snyder. */" -include_guard = "__COLORLOG_H" +header = "/* Copyright (C) 2020-2022 Micah Snyder. */" +include_guard = "__DEMORUST_H" autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */" sys_includes = [] includes = [] diff --git a/lib/colorlog/colorlog.rs b/lib/rust/src/colorlog.rs similarity index 62% rename from lib/colorlog/colorlog.rs rename to lib/rust/src/colorlog.rs index cc8af6e..d48f787 100644 --- a/lib/colorlog/colorlog.rs +++ b/lib/rust/src/colorlog.rs @@ -13,7 +13,7 @@ enum LogLevel { Error, } -fn clog(level: LogLevel, message: &str) -> () { +fn clog(level: LogLevel, message: &str) { match level { LogLevel::Debug => println!("Debug: {}", message.green()), LogLevel::Info => println!("{}", message), @@ -27,30 +27,51 @@ unsafe fn from_c_string<'t>(string: *const u8, string_len: usize) -> &'t str { std::str::from_utf8(slice as &[u8]).unwrap() } +/// Print a debug message using the clooooggg +/// +/// # Safety +/// +/// message must be a valid pointer to utf-8 encoded data of size message_len #[no_mangle] -pub unsafe extern "C" fn clog_debug(message: *const u8, message_len: usize) -> () { +pub unsafe extern "C" fn clog_debug(message: *const u8, message_len: usize) { clog(LogLevel::Debug, from_c_string(message, message_len)); } +/// Print a message using the clooooggg +/// +/// # Safety +/// +/// message must be a valid pointer to utf-8 encoded data of size message_len #[no_mangle] -pub unsafe extern "C" fn clog_info(message: *const u8, message_len: usize) -> () { +pub unsafe extern "C" fn clog_info(message: *const u8, message_len: usize) { clog(LogLevel::Info, from_c_string(message, message_len)); } +/// Print a warning message using the clooooggg +/// +/// # Safety +/// +/// message must be a valid pointer to utf-8 encoded data of size message_len #[no_mangle] -pub unsafe extern "C" fn clog_warning(message: *const u8, message_len: usize) -> () { +pub unsafe extern "C" fn clog_warning(message: *const u8, message_len: usize) { clog(LogLevel::Warning, from_c_string(message, message_len)); } +/// Print an error message using the clooooggg +/// +/// # Safety +/// +/// message must be a valid pointer to utf-8 encoded data of size message_len #[no_mangle] -pub unsafe extern "C" fn clog_error(message: *const u8, message_len: usize) -> () { +pub unsafe extern "C" fn clog_error(message: *const u8, message_len: usize) { clog(LogLevel::Error, from_c_string(message, message_len)); } #[cfg(test)] mod tests { + /// faux test to demonstrate running rust unit tests through CMake / CTest #[test] - fn it_works() { + fn test_colorlog() { assert_eq!(2 + 2, 4); } } diff --git a/lib/rust/src/do_thing.rs b/lib/rust/src/do_thing.rs new file mode 100644 index 0000000..863c37c --- /dev/null +++ b/lib/rust/src/do_thing.rs @@ -0,0 +1,33 @@ +/* + * Example module that uses cbindgen generated bindings (sys.rs) + * + * Copyright (C) 2022 Micah Snyder. + */ + +use crate::sys; + +/// A rust function that may be called from C, and also itself calls into C +/// +/// # Safety +/// +/// inout must be a valid pointer to some mutable data of size inout_len +#[no_mangle] +pub unsafe extern "C" fn do_thing_with_call_into_c(inout: *mut u8, inout_len: usize) -> bool { + sys::do_the_thing(inout, inout_len.try_into().unwrap()) +} + +#[cfg(test)] +mod tests { + use super::do_thing_with_call_into_c; + + #[test] + fn test_do_thing_with_call_into_c() { + let x = &mut [1, 2, 3]; + + let ret = unsafe { do_thing_with_call_into_c(x.as_mut_ptr(), x.len()) }; + + assert_eq!(ret, true); + + assert_eq!(x, &[2, 4, 6]); + } +} diff --git a/lib/rust/src/lib.rs b/lib/rust/src/lib.rs new file mode 100644 index 0000000..8304385 --- /dev/null +++ b/lib/rust/src/lib.rs @@ -0,0 +1,11 @@ +/* + * demo features ported to Rust + * + * Copyright (C) 2020-2022 Micah Snyder. + */ + +/// cbindgen:ignore +pub mod sys; + +pub mod colorlog; +pub mod do_thing; diff --git a/lib/rust/src/sys.rs b/lib/rust/src/sys.rs new file mode 100644 index 0000000..f5ac32f --- /dev/null +++ b/lib/rust/src/sys.rs @@ -0,0 +1,9 @@ +/* automatically generated by rust-bindgen 0.59.2 */ + +#![allow(non_snake_case, non_camel_case_types, non_upper_case_globals)] + +pub type __uint8_t = ::std::os::raw::c_uchar; +pub type size_t = ::std::os::raw::c_ulong; +extern "C" { + pub fn do_the_thing(inout: *mut u8, inout_size: size_t) -> bool; +} diff --git a/run-clang-format.sh b/run-clang-format.sh new file mode 100755 index 0000000..6fefdc8 --- /dev/null +++ b/run-clang-format.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +clang-format -style='{ Language: Cpp, UseTab: Never, IndentWidth: 4, AlignTrailingComments: true, AlignConsecutiveAssignments: true, AlignAfterOpenBracket: true, AlignEscapedNewlines: Left, AlignOperands: true, AllowShortFunctionsOnASingleLine: Empty, AllowShortIfStatementsOnASingleLine: true, AllowShortLoopsOnASingleLine: true, BreakBeforeBraces: Linux, BreakBeforeTernaryOperators: true, ColumnLimit: 0, FixNamespaceComments: true, SortIncludes: false, MaxEmptyLinesToKeep: 1, SpaceBeforeParens: ControlStatements, IndentCaseLabels: true, DerivePointerAlignment: true }' -dump-config > .clang-format + +find ./ -iname *.h -o -iname *.c | xargs clang-format -i -verbose diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000..f8487ae --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,18 @@ +# Copyright (C) 2020-2022 Micah Snyder. + +# +# Targets for all the application tests and main library unit tests. +# + +if(WIN32) + file(TO_NATIVE_PATH $ LIBDEMO) +else() + set(LIBDEMO $) +endif() + +set(ENVIRONMENT + LIBDEMO=${LIBDEMO} +) + +add_rust_test(NAME demorust WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/lib/rust") +set_property(TEST demorust PROPERTY ENVIRONMENT ${ENVIRONMENT})