├── .clippy.toml ├── .github ├── actions │ ├── setup-hwloc-dependencies │ │ └── action.yml │ └── system-information │ │ └── action.yml ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .tarpaulin.toml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── SECURITY.md ├── codecov.yml ├── examples ├── allocate_bound.rs ├── bind_process_cpu.rs ├── bind_threads_cpu.rs ├── bind_to_last_core.rs ├── number_of_packages.rs ├── process_cpu_bindings.rs ├── processor_cache.rs ├── support.rs ├── walk_linear.rs └── walk_tree.rs ├── hwlocality-sys ├── Cargo.toml ├── README.md ├── build.rs └── src │ └── lib.rs ├── src ├── bitmap │ ├── cow.rs │ ├── mod.rs │ ├── newtypes.rs │ └── reference.rs ├── cpu │ ├── binding.rs │ ├── cache.rs │ ├── cpuset.rs │ ├── kind.rs │ └── mod.rs ├── errors.rs ├── ffi │ ├── int.rs │ ├── mod.rs │ ├── string.rs │ ├── transparent.rs │ └── unknown.rs ├── info.rs ├── interop │ ├── linux.rs │ ├── mod.rs │ └── windows.rs ├── lib.rs ├── memory │ ├── attribute.rs │ ├── binding.rs │ ├── mod.rs │ └── nodeset.rs ├── object │ ├── attributes │ │ ├── bridge.rs │ │ ├── cache.rs │ │ ├── group.rs │ │ ├── mod.rs │ │ ├── numa.rs │ │ ├── osdev.rs │ │ └── pci.rs │ ├── depth.rs │ ├── distance.rs │ ├── hierarchy.rs │ ├── lists.rs │ ├── mod.rs │ ├── search │ │ ├── io.rs │ │ └── mod.rs │ └── types.rs ├── path.rs ├── strategies.rs └── topology │ ├── builder.rs │ ├── editor.rs │ ├── export │ ├── mod.rs │ ├── synthetic.rs │ └── xml.rs │ ├── mod.rs │ └── support.rs └── tests └── single-threaded.rs /.clippy.toml: -------------------------------------------------------------------------------- 1 | allow-unwrap-in-tests = true 2 | doc-valid-idents = [ 3 | "CoreType", "FrequencyMaxMHz", "InfiniBand", "NUMALatency", "NVLink", 4 | "NVLinkBandwidth", "NVSwitch", "NVSwitches", "OpenCL", "OpenFabrics", ".." 5 | ] 6 | semicolon-inside-block-ignore-singleline = true 7 | semicolon-outside-block-ignore-multiline = true 8 | -------------------------------------------------------------------------------- /.github/actions/setup-hwloc-dependencies/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Set up hwloc dependencies' 2 | description: 'Make sure pkgconfig and hwloc are available' 3 | inputs: 4 | hwloc-version: 5 | description: 'hwloc version to be installed' 6 | required: true 7 | hwloc-version-short: 8 | description: 'Shortened name of the hwloc version, used in download URLs' 9 | required: true 10 | runs: 11 | using: "composite" 12 | steps: 13 | # TODO: Investigate why Ubuntu builds need liblzma-dev since 24.04. 14 | # This is surprising since hwloc itself doesn't need it. 15 | - name: Force-install liblzma-dev (Ubuntu) 16 | if: runner.os == 'Linux' 17 | uses: Eeems-Org/apt-cache-action@v1 18 | with: 19 | packages: liblzma-dev 20 | 21 | - name: Cache hwloc install (Unices) 22 | if: runner.os != 'Windows' 23 | id: cache-deps-unix 24 | uses: actions/cache@v4 25 | with: 26 | path: ~/hwloc 27 | key: ${{ runner.os }}-deps-hwloc${{ inputs.hwloc-version }} 28 | 29 | - name: Cache all dependencies (Windows) 30 | if: runner.os == 'Windows' 31 | id: cache-deps-win 32 | uses: actions/cache@v4 33 | with: 34 | path: | 35 | ~/coreinfo 36 | ~/hwloc 37 | ~/pkgconfig 38 | key: ${{ runner.os }}-deps-pkgconfig0.28-1-hwloc${{ inputs.hwloc-version }} 39 | 40 | - name: Install dependencies (Windows) 41 | if: runner.os == 'Windows' && steps.cache-deps-win.outputs.cache-hit != 'true' 42 | shell: pwsh 43 | run: | 44 | $client = new-object System.Net.WebClient 45 | 46 | # Download/unpack sysinternals coreinfo, for system config probing 47 | $client.DownloadFile( 48 | "https://download.sysinternals.com/files/Coreinfo.zip", 49 | "coreinfo.zip" 50 | ) 51 | mkdir "${env:USERPROFILE}/coreinfo" 52 | Expand-Archive -Path coreinfo.zip -DestinationPath "${env:USERPROFILE}/coreinfo" 53 | 54 | # Download/unpack pkgconfiglite as a windows pkgconfig impl 55 | # FIXME: Working around broken sourceforge downloads with a savage mirror 56 | $client.DownloadFile( 57 | "https://github.com/HadrienG2/hwlocality/files/11331438/pkg-config-lite-0.28-1_bin-win32.zip", 58 | "pkg-config.zip" 59 | ) 60 | Expand-Archive -Path pkg-config.zip -DestinationPath . 61 | mv "pkg-config-lite-0.28-1" "${env:USERPROFILE}/pkgconfig" 62 | 63 | # Download/unpack hwloc binary release 64 | $client.DownloadFile( 65 | "https://download.open-mpi.org/release/hwloc/v${{ inputs.hwloc-version-short }}/hwloc-win64-build-${{ inputs.hwloc-version }}.zip", 66 | "hwloc.zip" 67 | ) 68 | Expand-Archive -Path hwloc.zip -DestinationPath . 69 | $HWLOC_PREFIX = "${env:USERPROFILE}/hwloc" 70 | mv "hwloc-win64-build-${{ inputs.hwloc-version }}" "$HWLOC_PREFIX" 71 | 72 | # hwloc's pkgconfig follows unix convention of not adding a lib 73 | # prefix to library names, but unfortunately MSVC does not add one 74 | # either, so we must either patch the pkgconfig (as done for prefix 75 | # below) or add a non-lib-prefixed version of the library. 76 | cp "$HWLOC_PREFIX/lib/libhwloc.lib" "$HWLOC_PREFIX/lib/hwloc.lib" 77 | 78 | # hwloc's pkgconfig is also not relocatable, so we must relocate it 79 | # ourselves using the powershell equivalent of a sed 80 | $HWLOC_PKGCONFIG = "$HWLOC_PREFIX/lib/pkgconfig/hwloc.pc" 81 | echo "=== Initial pkg config ===" 82 | cat "$HWLOC_PKGCONFIG" 83 | echo "==========================" 84 | (Get-Content "$HWLOC_PKGCONFIG") | 85 | ForEach-Object { $_ -replace "^prefix=.*$", "prefix=$HWLOC_PREFIX" } | 86 | Set-Content "$HWLOC_PKGCONFIG" 87 | echo "=== Patched pkg config ===" 88 | cat "$HWLOC_PKGCONFIG" 89 | echo "==========================" 90 | 91 | - name: Install hwloc (macOS) 92 | if: runner.os == 'macOS' && steps.cache-deps-unix.outputs.cache-hit != 'true' 93 | shell: bash 94 | run: | 95 | curl -L "https://download.open-mpi.org/release/hwloc/v${{ inputs.hwloc-version-short }}/hwloc-${{ inputs.hwloc-version }}.tar.gz" | tar -xz 96 | cd hwloc-${{ inputs.hwloc-version }} 97 | ./configure --prefix="${HOME}/hwloc" --enable-debug 98 | make -j$(nproc) 99 | make install 100 | cd .. 101 | rm -rf ${{ inputs.hwloc-version }} 102 | 103 | - name: Install hwloc (other OSes) 104 | if: runner.os != 'Windows' && runner.os != 'macOS' && steps.cache-deps-unix.outputs.cache-hit != 'true' 105 | shell: bash 106 | run: | 107 | curl -L "https://download.open-mpi.org/release/hwloc/v${{ inputs.hwloc-version-short }}/hwloc-${{ inputs.hwloc-version }}.tar.gz" | tar -xz 108 | cd hwloc-${{ inputs.hwloc-version }} 109 | ./configure --prefix="${HOME}/hwloc" --enable-debug --enable-static 110 | make -j$(nproc) 111 | make install 112 | cd .. 113 | rm -rf ${{ inputs.hwloc-version }} 114 | 115 | - name: Bring dependencies into PATHs (Unices) 116 | if: runner.os != 'Windows' 117 | shell: bash 118 | run: | 119 | HWLOC_PREFIX="${HOME}/hwloc" 120 | echo "${HWLOC_PREFIX}/bin" >> "$GITHUB_PATH" 121 | echo "PKG_CONFIG_PATH=${HWLOC_PREFIX}/lib/pkgconfig:${PKG_CONFIG_PATH}" >> "$GITHUB_ENV" 122 | 123 | - name: Bring dependencies into PATHs (Windows) 124 | if: runner.os == 'Windows' 125 | shell: pwsh 126 | run: | 127 | $HWLOC_PREFIX = "${env:USERPROFILE}/hwloc" 128 | printf "%s\n" "${env:USERPROFILE}/coreinfo" >> ${env:GITHUB_PATH} 129 | printf "%s\n" "${env:USERPROFILE}/pkgconfig/bin" >> ${env:GITHUB_PATH} 130 | printf "%s\n%s\n" "$HWLOC_PREFIX/bin" "$HWLOC_PREFIX/lib" >> ${env:GITHUB_PATH} 131 | printf "%s\n" "PKG_CONFIG_PATH=$HWLOC_PREFIX/lib/pkgconfig;${env:PKG_CONFIG_PATH}" >> ${env:GITHUB_ENV} 132 | -------------------------------------------------------------------------------- /.github/actions/system-information/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Query system information' 2 | description: 'Collect useful system info for hwloc bug reports' 3 | runs: 4 | using: "composite" 5 | steps: 6 | 7 | - name: Query system information (macOS) 8 | if: runner.os == 'macOS' 9 | shell: bash 10 | run: | 11 | sysctl hw 12 | sysctl machdep 13 | 14 | - name: Query system information (Linux) 15 | if: runner.os == 'Linux' 16 | shell: bash 17 | run: | 18 | hwloc-gather-cpuid 19 | for f in cpuid/*; do 20 | printf "\n=== Output file %s ===\n" "${f}" 21 | cat "${f}" 22 | done 23 | 24 | - name: Query system information (Windows) 25 | if: runner.os == 'Windows' 26 | shell: pwsh 27 | run: | 28 | coreinfo64 -accepteula -cgnlsm 29 | hwloc-gather-cpuid 30 | echo "" 31 | echo "=== Output files follow ===" 32 | Dir -Recurse cpuid | ForEach-Object { 33 | echo "" 34 | Get-Childitem $_ 35 | type $_ 36 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | - package-ecosystem: github-actions 10 | directories: 11 | - /.github/workflows 12 | - /.github/actions/setup-hwloc-dependencies 13 | - /.github/actions/system-information 14 | schedule: 15 | interval: weekly 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binary outputs 2 | /target 3 | 4 | # macOS caches 5 | **/*.DS_Store 6 | 7 | # IntelliJ internals 8 | .idea 9 | **/*.bk 10 | 11 | # Code coverage 12 | /*.profraw 13 | /tarpaulin-report.html 14 | 15 | # It normally does not make sense to share proptest regression files across 16 | # machines because they are largely specific to a machine's topology. 17 | /proptest-regressions/ 18 | 19 | # Mutation testing 20 | /mutants.out/ 21 | -------------------------------------------------------------------------------- /.tarpaulin.toml: -------------------------------------------------------------------------------- 1 | [release_like_test] 2 | fail-immediately = true 3 | features = "hwloc-latest" 4 | implicit-test-threads = true 5 | profile = "release_like_test" 6 | skip-clean = true 7 | workspace = true 8 | 9 | [normal_test] 10 | fail-immediately = true 11 | features = "hwloc-latest" 12 | implicit-test-threads = true 13 | skip-clean = true 14 | workspace = true 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["hwlocality-sys"] 3 | 4 | [workspace.package] 5 | authors = ["Hadrien G. ", "Joseph Hirschfeld ", "Michael Nitschinger "] 6 | edition = "2021" 7 | rust-version = "1.75.0" 8 | repository = "https://github.com/HadrienG2/hwlocality" 9 | license = "MIT" 10 | keywords = ["hwloc", "locality", "cache", "numa", "hardware"] 11 | categories = ["concurrency", "external-ffi-bindings", "hardware-support", "memory-management", "os"] 12 | 13 | [workspace.dependencies] 14 | proptest = { version = "1.8", default-features = false, features = ["std"] } 15 | libc = "0.2" 16 | static_assertions = "1.1" 17 | windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_System_Threading"] } 18 | 19 | [profile.test.package.proptest] 20 | opt-level = 3 21 | 22 | [profile.test.package.rand_chacha] 23 | opt-level = 3 24 | 25 | [profile.test.package.similar] 26 | opt-level = 3 27 | 28 | # This profile runs tests without the assertions that are normally present in 29 | # debug mode and used to detect release mode in code. This way we can test both 30 | # the debug and release code paths of arithmetic ops without suffering the 31 | # slowness and coverage quality degradation that real release builds incur. 32 | [profile.release_like_test] 33 | inherits = "test" 34 | debug-assertions = false 35 | overflow-checks = false 36 | 37 | [package] 38 | name = "hwlocality" 39 | version = "1.0.0-alpha.11" 40 | authors.workspace = true 41 | edition.workspace = true 42 | rust-version.workspace = true 43 | description = "Idiomatic Rust bindings for the hwloc hardware locality library" 44 | repository.workspace = true 45 | license.workspace = true 46 | keywords.workspace = true 47 | categories.workspace = true 48 | 49 | [features] 50 | # Require the latest release of hwloc supported by this library 51 | # 52 | # It is not recommended to use this feature in production, since an update to 53 | # hwlocality can silently bump your hwloc build requirement. But this feature is 54 | # convenient for scenarios like CI where you want to test with all features on. 55 | hwloc-latest = ["hwloc-2_12_1"] 56 | 57 | # Require at least hwloc v2.0.4, which enables... 58 | # 59 | # - Checking if the merging of group objects with identical parents or children 60 | # has been prevented. 61 | hwloc-2_0_4 = ["hwlocality-sys/hwloc-2_0_4"] 62 | 63 | # Require at least hwloc v2.1.0, which enables... 64 | # 65 | # - Support for ObjectType::Die, used to model CPUs with multiple dies per package. 66 | # - Support for ObjectType::MemCache, used to model memory-side caches. 67 | # - Blacklisting some components from being enabled in a topology via 68 | # TopologyBuilder::blacklist_component(), for faster topology loading. 69 | # 70 | # ...in addition to all features listed above 71 | hwloc-2_1_0 = ["hwlocality-sys/hwloc-2_1_0", "hwloc-2_0_4"] 72 | 73 | # Require at least hwloc v2.2.0, which enables... 74 | # 75 | # - Removing SMT from a given CpuSet using CpuSet::singlify_per_core. 76 | # - PCI devices with 32-bit domain indices. Since we currently can't tell which 77 | # configuration was used in the hwloc build, we assume the default 78 | # configuration, which is to disable it until v3.0 where it becomes the default. 79 | # 80 | # ...in addition to all features listed above 81 | hwloc-2_2_0 = ["hwlocality-sys/hwloc-2_2_0", "hwloc-2_1_0"] 82 | 83 | # Require at least hwloc v2.3.0, which enables... 84 | # 85 | # - Memory attributes, allowing latency/bandwidth queries between initiators 86 | # (e.g. CPU cores) and NUMA nodes. 87 | # - Topology::local_numa_nodes() for listing NUMA nodes that are local to some 88 | # locality. 89 | # - Export of support information in XML, which can be read back by setting the 90 | # new BuildFlags::IMPORT_SUPPORT flag. 91 | # - Modifying topologies using TopologyEditor. This was actually supported by 92 | # previous hwloc versions, but in a manner that violates the Rust aliasing 93 | # model so we could not expose it in the Rust bindings. 94 | # - Cloning topologies, which was not legal under the Rust aliasing model in 95 | # previous hwloc versions for similar reasons as topology editing. 96 | # 97 | # ...in addition to all features listed above 98 | hwloc-2_3_0 = ["hwlocality-sys/hwloc-2_3_0", "hwloc-2_2_0"] 99 | 100 | # Require at least hwloc v2.4.0, which enables... 101 | # 102 | # - Discriminating different CPU kinds, for hybrid CPU support. 103 | # 104 | # ...in addition to all features listed above 105 | hwloc-2_4_0 = ["hwlocality-sys/hwloc-2_4_0", "hwloc-2_3_0"] 106 | 107 | # Require at least hwloc v2.5.0, which enables... 108 | # 109 | # - Querying Windows processor groups. 110 | # - Converting between objects with same locality, e.g. NUMA nodes and Packages, 111 | # using Topology::object_with_same_locality. 112 | # - Modifying distance structures using Distances::transform() 113 | # - Adding distances between objects using TopologyEditor::add_distances(). This 114 | # was actually possible in previous versions of hwloc, but the old API was 115 | # quite different, and is not currently supported by the Rust bindings. 116 | # - New BuildFlags to prevent modifications of CPU and memory bindings during 117 | # topology discovery, especially on Windows. 118 | # 119 | # ...in addition to all features listed above 120 | hwloc-2_5_0 = ["hwlocality-sys/hwloc-2_5_0", "hwloc-2_4_0"] 121 | 122 | # Require at least hwloc v2.8.0, which enables... 123 | # 124 | # - BuildFlags to disable the enumeration of some topology metadata in order 125 | # to speed up topology building when they are not needed. 126 | # - Dedicated memory attributes for read and write bandwidth and latency, rather 127 | # than an overall figure of merit. 128 | # 129 | # ...in addition to all features listed above 130 | hwloc-2_8_0 = ["hwlocality-sys/hwloc-2_8_0", "hwloc-2_5_0"] 131 | 132 | # Require at least hwloc v2.10.0 133 | # 134 | # This is a maintenance release which does not affect the external hwlocality 135 | # API, but cleans up hwlocality internals. 136 | # 137 | # ...in addition to all features listed above 138 | hwloc-2_10_0 = ["hwlocality-sys/hwloc-2_10_0", "hwloc-2_8_0"] 139 | 140 | # Require at least hwloc v2.11.0 141 | # 142 | # - MemoryBindingPolicy::WeightedInterleave is now available on Linux 6.9+, with 143 | # a matching MemoryBindingSupport::weighted_interleave_policy() query. 144 | # - TopologyEditor::insert_group_object() can finally expose a safe way to set 145 | # a group object's subtype on Windows, thanks to official support from hwloc. 146 | # 147 | # ...in addition to all features listed above 148 | hwloc-2_11_0 = ["hwlocality-sys/hwloc-2_11_0", "hwloc-2_10_0"] 149 | 150 | # Require at least hwloc v2.12.0 151 | # 152 | # - Topology::get_default_nodeset() is now available as a way to query the 153 | # "default" NUMA nodes, which are the ones on which memory allocations should 154 | # be made unless particular requirements dictate otherwise. 155 | # 156 | # ...in addition to all features listed above 157 | hwloc-2_12_0 = ["hwlocality-sys/hwloc-2_12_0", "hwloc-2_11_0"] 158 | 159 | # Require at least hwloc v2.12.1 160 | # 161 | # - LocalNUMANodeFlags::INTERSECT_LOCALITY is now available as a way to select 162 | # NUMA nodes that intersect the target CPU set: they cover some of the target 163 | # CPU cores, but not all of them, and they also cover additional CPU cores 164 | # 165 | # ...in addition to all features listed above 166 | hwloc-2_12_1 = ["hwlocality-sys/hwloc-2_12_1", "hwloc-2_12_0"] 167 | 168 | # WIP hwloc v3.0.0 support, do not use yet! 169 | hwloc-3_0_0 = ["hwlocality-sys/hwloc-3_0_0", "hwloc-2_12_1"] 170 | 171 | # To support a new hwloc release that introduces API or ABI changes (see the 172 | # NEWS file in the hwloc source tree for release notes)... 173 | # 174 | # - Add a new feature matching that release version. 175 | # - Make it enable features associated with all previous versions. 176 | # - Change hwloc-latest to point to that feature. 177 | # - Update hwlocality-sys' Cargo.toml similarly 178 | # - Update hwlocality-sys' build.rs to... 179 | # * Record the new feature's hwloc version requirements in `setup_hwloc()` 180 | # * Use a newer official tarball, with appropriate checksum (can be computed 181 | # using `curl | sha3-256sum`), in `setup_vendored_hwloc()` 182 | # * If this is a new major version, add support for it in `find_hwloc()` 183 | # - Make the binding honor the API/ABI changes with the right cfg()s. 184 | # - Add this feature to the all CI matrices. 185 | # - Bump the hwloc version that's installed by CI. 186 | # - Once CI has run at least once, add new matrix entry to required checks. 187 | # - Tag new minor releases of hwlocality and hwlocality-sys 188 | 189 | # Automatically download and build a compatible hwloc version internally. 190 | # Otherwise, hwlocality will use the system installation of hwloc. 191 | # 192 | # By default, hwloc is built with all optional features disabled, which 193 | # minimizes the odds of hard-to-debug vendored build failures. You may instead 194 | # let the hwloc build system automatically enable optional features, as it does 195 | # by default, using the "vendored-extra" cargo feature. 196 | vendored = ["hwlocality-sys/vendored"] 197 | 198 | # Let the hwloc vendored build auto-enable optional features (libxml2, I/O 199 | # device discovery...) as the build system deems appropriate for the host. 200 | # 201 | # Be warned that this autodetection process is known to have false positives, 202 | # e.g. CUDA support may end up being enabled even when the relevant CUDA toolkit 203 | # is not in PATHs, resulting in hwloc build failures. 204 | vendored-extra = ["hwlocality-sys/vendored-extra", "vendored"] 205 | 206 | # Implement required infrastructure for property-based testing 207 | proptest = ["dep:proptest"] 208 | 209 | [dependencies] 210 | # === Last dependency usage review performed 2023-09-30 === 211 | 212 | # Used to store bounded-length variable-sized results in cache analysis 213 | arrayvec = "0.7" 214 | 215 | # Used to interface hwloc's C flags 216 | bitflags = "2.9" 217 | 218 | # Used to simplify the implementation of newtypes 219 | derive_more = { version = "2.0", default-features = false, features = ["as_ref", "deref", "display", "from", "into", "into_iterator", "not"] } 220 | 221 | # Used to interpret and report hwloc error codes 222 | errno = "0.3" 223 | 224 | # Part of this project (raw FFI binding) 225 | hwlocality-sys = { path = "./hwlocality-sys" } 226 | 227 | # Used for malloc, free, OS typedefs, and example thread queries 228 | libc.workspace = true 229 | 230 | # Used to interface hwloc enums + for proptests and topology debug output 231 | strum = { version = "0.27.2", features = ["derive"] } 232 | 233 | # Used to simplify error reporting 234 | thiserror = "2.0" 235 | 236 | # Used for optional proptest feature 237 | proptest = { workspace = true, optional = true } 238 | 239 | [target.'cfg(windows)'.dependencies] 240 | # Used for current_thread_id 241 | windows-sys.workspace = true 242 | 243 | [dev-dependencies] 244 | # Used to simplify examples and test error reporting 245 | eyre = "0.6" 246 | 247 | # Used to ease debugging of string tests 248 | similar-asserts = "1.7" 249 | 250 | # Used for random testing 251 | proptest.workspace = true 252 | 253 | # Used to check trait implementations 254 | static_assertions.workspace = true 255 | 256 | # Used in examples to portably query the OS process list 257 | sysinfo = { version = "0.36", default-features = false, features = ["system"] } 258 | 259 | # Used to test file I/O 260 | tempfile = "3.20" 261 | 262 | # Used to trace control flow in some tests to make failure investigation easier 263 | tracing = "0.1.41" 264 | tracing-subscriber = { version = "0.3.19", default-features = false, features = ["std", "fmt"] } 265 | tracing-error = { version = "0.2.1", default-features = false } 266 | 267 | [package.metadata.docs.rs] 268 | all-features = true 269 | rustdoc-args = ["--cfg", "docsrs"] 270 | 271 | [lints.rust] 272 | unexpected_cfgs = { level = "warn", check-cfg = ["cfg(tarpaulin_include)"] } 273 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2015 hwloc-rs, 2020 Joseph Hirschfeld 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is furnished 9 | to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20 | IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hwlocality: Rust bindings for the hwloc library 2 | 3 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) 4 | [![Package on crates.io](https://img.shields.io/crates/v/hwlocality.svg)](https://crates.io/crates/hwlocality) 5 | [![Documentation](https://docs.rs/hwlocality/badge.svg)](https://docs.rs/hwlocality/) 6 | [![Continuous Integration](https://img.shields.io/github/actions/workflow/status/HadrienG2/hwlocality/ci.yml?branch=main)](https://github.com/HadrienG2/hwlocality/actions?query=workflow%3A%22Continuous+Integration%22) 7 | [![Code coverage](https://codecov.io/gh/HadrienG2/hwlocality/graph/badge.svg?token=OYWLNUD9AI)](https://codecov.io/gh/HadrienG2/hwlocality) 8 | [![CII Best Practices Summary](https://img.shields.io/cii/summary/7876)](https://www.bestpractices.dev/en/projects/7876) 9 | ![Requires rustc 1.75.0+](https://img.shields.io/badge/rustc-1.75.0+-lightgray.svg) 10 | 11 | ## What is this? 12 | 13 | To optimize programs for parallel performance on all hardware, you must accept 14 | that on many common platforms, 15 | [symmetric multiprocessing](https://en.wikipedia.org/wiki/Symmetric_multiprocessing) 16 | is a lie. "CPUs" detected by the operating system often have inequal access 17 | to shared resources like caches, DRAM and I/O peripherals, sometimes even 18 | inequal specifications (as in Arm big.LITTLE, Apple Mx and Intel Adler Lake), 19 | and significant performance gains can be achieved by taking these facts into 20 | account in your code. 21 | 22 | This is the latest maintained Rust binding to 23 | [hwloc](http://www.open-mpi.org/projects/hwloc), a C library from Open MPI 24 | for detecting the hierarchical topology of modern architectures: NUMA memory 25 | nodes, sockets, shared data & instruction caches, cores, simultaneous multi 26 | threading, and more. Additionally, hwloc lets you pin threads to specific CPU 27 | cores and memory to specific NUMA nodes, which is a prerequisite to perform 28 | topology-aware program optimizations. 29 | 30 | `hwlocality` is based on and still shares some code and design with the 31 | previous, now-unmaintained attempts to write Rust hwloc bindings at 32 | [Ichbinjoe/hwloc2-rs](https://github.com/Ichbinjoe/hwloc2-rs) and 33 | [daschl/hwloc-rs](https://github.com/daschl/hwloc-rs). However, it does not aim 34 | for API compatibility with them. Indeed, many changes have been made with 35 | respect to hwloc2-rs in the aim of improving ergonomics, performance, and 36 | removing avenues for Undefined Behaviour like assuming pointers are non-null or 37 | union fields are valid when nobody tells you they will always be. 38 | 39 | ## Prerequisites 40 | 41 | `hwlocality` is compatible with `libhwloc` v2.0 and later. You can install a 42 | suitable version of `libhwloc` in two different ways: 43 | 44 | 1. If your package manager of choice provides a reasonably recent `libhwloc` 45 | package, then you can install it along with the associated development 46 | package (typically called `libhwloc-dev` or `libhwloc-devel`). This is the 47 | recommended way to do things because it will greatly speed up your 48 | `hwlocality` (re)builds and allow you to easily keep `libhwloc` up to date 49 | along with the rest of your development environment. 50 | 2. If you cannot use the above method for any reason, then `hwlocality` can 51 | alternatively download and build its own copy of `libhwloc`. To use such an 52 | internal build, please enable the `vendored` Cargo feature. In addition to a 53 | working C build environment, you will need `automake` and `libtool` on 54 | Unices, and `cmake` on Windows. 55 | 56 | Unless you are using a vendored version of hwloc of Windows, you will also need 57 | to install `pkg-config` or one of its clones (`pkgconf`, `pkgconfiglite`...), as 58 | it is used to find `libhwloc` and set up `hwlocality` to link against it. 59 | 60 | By default, compatibility with all hwloc 2.x versions is aimed for, which means 61 | features from newer versions in the 2.x series (or, in the near future, 62 | compatibility with breaking changes from the 3.x series) are not supported by 63 | default. 64 | 65 | You can enable them, at the cost of losing compatibility with older 66 | hwloc 2.x releases, by enabling the cargo feature that matches the lowest hwloc 67 | release you need to be compatible with. See [the `[features]` section of this 68 | crate's Cargo.toml](https://github.com/hadrieng2/hwlocality/tree/main/Cargo.toml#L15) 69 | for more information. 70 | 71 | ## Usage 72 | 73 | First, add `hwlocality` as a dependency: 74 | 75 | ```bash 76 | cargo add hwlocality 77 | ``` 78 | 79 | Then, inside of your code, set up a 80 | [`Topology`](https://docs.rs/hwlocality/latest/hwlocality/topology/struct.Topology.html). 81 | This is the main entry point to the hwloc library, through which you can access 82 | almost every operation that hwloc allows. 83 | 84 | Here is a quick usage example which walks though the detected hardware topology 85 | and prints out a short description of every CPU and cache object known to hwloc: 86 | 87 | ```rust 88 | use hwlocality::{object::depth::NormalDepth, Topology}; 89 | 90 | fn main() -> eyre::Result<()> { 91 | let topology = Topology::new()?; 92 | 93 | for depth in NormalDepth::iter_range(NormalDepth::MIN, topology.depth()) { 94 | println!("*** Objects at depth {depth}"); 95 | 96 | for (idx, object) in topology.objects_at_depth(depth).enumerate() { 97 | println!("{idx}: {object}"); 98 | } 99 | } 100 | 101 | Ok(()) 102 | } 103 | ``` 104 | 105 | One possible output is: 106 | 107 | ```text 108 | *** Objects at depth 0 109 | 0: Machine 110 | *** Objects at depth 1 111 | 0: Package 112 | *** Objects at depth 2 113 | 0: L3 (16MB) 114 | *** Objects at depth 3 115 | 0: L2 (512KB) 116 | 1: L2 (512KB) 117 | 2: L2 (512KB) 118 | 3: L2 (512KB) 119 | 4: L2 (512KB) 120 | 5: L2 (512KB) 121 | *** Objects at depth 4 122 | 0: L1d (32KB) 123 | 1: L1d (32KB) 124 | 2: L1d (32KB) 125 | 3: L1d (32KB) 126 | 4: L1d (32KB) 127 | 5: L1d (32KB) 128 | *** Objects at depth 5 129 | 0: Core 130 | 1: Core 131 | 2: Core 132 | 3: Core 133 | 4: Core 134 | 5: Core 135 | *** Objects at depth 6 136 | 0: PU 137 | 1: PU 138 | 2: PU 139 | 3: PU 140 | 4: PU 141 | 5: PU 142 | 6: PU 143 | 7: PU 144 | 8: PU 145 | 9: PU 146 | 10: PU 147 | 11: PU 148 | ``` 149 | 150 | More examples are available [in the source 151 | repository](https://github.com/hadrieng2/hwlocality/tree/main/examples). 152 | 153 | ## hwloc API coverage 154 | 155 | Most of the features from the hwloc 2.x series are now exposed by hwlocality. 156 | But some specialized features, mostly related to interoperability with other 157 | APIs, could not make it for various reasons. [Issues with the "api coverage" 158 | label](https://github.com/HadrienG2/hwlocality/issues?q=is%3Aopen+is%3Aissue+label%3A%22api+coverage%22) 159 | track unimplemented features, and are a great place to look for potential 160 | contributions to this library if you have time! 161 | 162 | If you are already familiar with the hwloc C API, you will also be happy to know 163 | that [`#[doc(alias)]` attributes](https://doc.rust-lang.org/rustdoc/advanced-features.html#add-aliases-for-an-item-in-documentation-search) 164 | are extensively used so that you can search the documentation for hwloc API 165 | entities like `hwloc_bitmap_t`, `hwloc_set_cpubind` or `hwloc_obj::arity` and 166 | be redirected to the suggested replacement in the Rust API. 167 | 168 | The main exceptions to this rule are notions that are not needed in Rust due to 169 | ergonomics improvements permitted by the Rust type system. For example... 170 | 171 | - C-style manual destructors are replaced by `Drop` impls 172 | - Type argument clarification flags like `HWLOC_MEMBIND_BYNODESET` are replaced 173 | by generics that do the right thing automatically. 174 | 175 | ## License 176 | 177 | This project uses the MIT license, please see the 178 | [LICENSE](https://github.com/hadrieng2/hwlocality/blob/main/LICENSE) file for 179 | more information. 180 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | This project aims for strict [SemVer](https://semver.org/) compliance. This 6 | means that upgrading to a new minor or patch version should be trivial, but 7 | upgrading to a new major version may incur some significant costs on your side. 8 | 9 | With that in mind, if we denote major.minor.patch the project's version 10 | number, the policy for security updates support is that... 11 | 12 | - The latest major.minor.patch version published on crates.io is supported 13 | (obviously). 14 | - Each previously published major release remains supported, for users of the 15 | latest minor.patch version, during 6 months after the release of the next 16 | major release. 17 | 18 | 19 | ## Reporting a Vulnerability 20 | 21 | ### Basics 22 | 23 | Please do not report security vulnerabilities (any bug that has security 24 | implications if e.g. a setuid binary happens to use `hwlocality`) using public 25 | channels such as... 26 | 27 | - A project's issue tracker 28 | - Social media 29 | - A [CVE 30 | Report](https://en.wikipedia.org/wiki/Common_Vulnerabilities_and_Exposures) 31 | 32 | Doing so is the computer security equivalent of reporting a fire safety risk by 33 | starting a fire: you are informing attackers that software has a problem before 34 | a fix is available, and they will be able to exploit this information for the 35 | entire duration it will take us to come up with a fix, which is [Very 36 | Bad](https://en.wikipedia.org/wiki/Zero-day_(computing)). 37 | 38 | Instead, you should privately report the vulnerability to the project, give it 39 | some time to come up with a fix, and only report the vulnerability publicly 40 | once a patch release is available for all supported major versions (except in 41 | worst-case scenarios discussed below). 42 | 43 | ### Reporting to the right project 44 | 45 | Please start by assessing whether the vulnerability only affects users of the 46 | `hwlocality` Rust bindings, or would also affect direct users of the underlying 47 | `hwloc` C library. 48 | 49 | If the problem lies in `hwloc` itself, the preferred course of action is to 50 | [directly report the vulnerability to the `hwloc` 51 | project](https://github.com/open-mpi/hwloc/security), so that it can be fixed 52 | with the broadest user impact. To avoid effort duplication between the two 53 | projects, mitigation of such issues at the `hwlocality` level will only be 54 | considered if you have correctly followed the above procedure and can 55 | convincingly argue that the `hwloc` developers are not responding to your 56 | vulnerability report in an appropriate and timely manner. 57 | 58 | If the problem only affects `hwlocality` users, or if `hwloc` declined to 59 | address the problem on their side, then please [privately submit us a security 60 | advisory](https://github.com/HadrienG2/hwlocality/security/advisories/new) that 61 | describes the vulnerability, assesses its impact, and provides instructions on 62 | how to replicate the issue locally. If you have a patch that adresses the 63 | issue, please also submit it there. 64 | 65 | ### Worst-case scenarios 66 | 67 | While private reporting and embargo until a bugfix is available is the preferred 68 | course of action on our side, we are well aware that if we spend too much time 69 | without fixing the vulnerability, or if you become aware that another attacker 70 | has found out about the vulnerability and started exploiting it, you may want to 71 | publicly report the vulnerability anyway. 72 | 73 | Here's what you can expect from us in terms of response time : 74 | 75 | - At the time of writing, the project only has one permanent maintainer, 76 | [**@HadrienG2**](https://github.com/HadrienG2). I will aim for fast replies 77 | (less than a week, ideally next-day), but I may be momentarily unavailable 78 | for various reasons: holidays, sickness... So please ping me a few times 79 | (e.g. after a week and a month) before concluding that I am unresponsive and 80 | must have abandoned the project. 81 | - Unless the vulnerability is extremely complex and requires a major library 82 | rewrite to fix, 90 days should be more than enough time for me to fix it. 83 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "hwlocality-sys/build.rs" 3 | 4 | codecov: 5 | notify: 6 | after_n_builds: 2 7 | 8 | coverage: 9 | status: 10 | project: 11 | default: 12 | threshold: 0.2 13 | -------------------------------------------------------------------------------- /examples/allocate_bound.rs: -------------------------------------------------------------------------------- 1 | use eyre::eyre; 2 | use hwlocality::{ 3 | memory::binding::{MemoryBindingFlags, MemoryBindingPolicy}, 4 | object::{depth::Depth, TopologyObject}, 5 | Topology, 6 | }; 7 | 8 | /// Allocate 4 MiB of memory that is bound to the last NUMA node on the system 9 | fn main() -> eyre::Result<()> { 10 | // Create topology and check feature support 11 | let topology = Topology::new()?; 12 | let Some(support) = topology.feature_support().memory_binding() else { 13 | println!("This example requires memory binding support"); 14 | return Ok(()); 15 | }; 16 | if !(support.allocate_bound() || support.set_current_process() || support.set_current_thread()) 17 | { 18 | println!( 19 | "This example needs support for querying and setting current process CPU bindings" 20 | ); 21 | return Ok(()); 22 | } 23 | 24 | // Find the last NUMA node on the system and 25 | let nodeset = last_node(&topology)? 26 | .nodeset() 27 | .ok_or_else(|| eyre!("NUMA nodes should have nodesets"))?; 28 | 29 | // Allocate memory that is bound to this NUMA node, binding ourselves to 30 | // it if necessary. 31 | const ALLOC_SIZE: usize = 4 * 1024 * 1024; 32 | println!("Will now allocate {ALLOC_SIZE} bytes of memory bound to NUMA node {nodeset}"); 33 | let _bytes = topology.binding_allocate_memory( 34 | ALLOC_SIZE, 35 | nodeset, 36 | MemoryBindingPolicy::default(), 37 | MemoryBindingFlags::ASSUME_SINGLE_THREAD, 38 | )?; 39 | 40 | Ok(()) 41 | } 42 | 43 | /// Find the last NUMA node one the system 44 | fn last_node(topology: &Topology) -> eyre::Result<&TopologyObject> { 45 | let mut all_nodes = topology.objects_at_depth(Depth::NUMANode); 46 | all_nodes 47 | .next_back() 48 | .ok_or_else(|| eyre!("at least one NUMA node should be present")) 49 | } 50 | -------------------------------------------------------------------------------- /examples/bind_process_cpu.rs: -------------------------------------------------------------------------------- 1 | use eyre::eyre; 2 | use hwlocality::{ 3 | cpu::binding::CpuBindingFlags, 4 | object::{types::ObjectType, TopologyObject}, 5 | Topology, 6 | }; 7 | 8 | /// Example which binds an arbitrary process (in this example this very same one) 9 | /// to the last processing unit (core or hyperthread). 10 | fn main() -> eyre::Result<()> { 11 | // Create topology and check feature support 12 | let topology = Topology::new()?; 13 | let Some(support) = topology.feature_support().cpu_binding() else { 14 | println!("This example requires CPU binding support"); 15 | return Ok(()); 16 | }; 17 | if !(support.get_process() && support.set_process()) { 18 | println!("This example needs support for querying and setting process CPU bindings"); 19 | return Ok(()); 20 | } 21 | 22 | // FIXME: hwloc's get_proc_cpu_binding() mysteriously fails on Windows CI. 23 | // May want to try again once this upstream issue is resolved: 24 | // https://github.com/open-mpi/hwloc/issues/78 25 | if cfg!(target_os = "windows") { 26 | println!( 27 | "This example currently fails on Windows for unknown reasons, and has been disabled" 28 | ); 29 | return Ok(()); 30 | } 31 | 32 | // Determine the active process' PID 33 | let pid = std::process::id(); 34 | println!("Binding Process with PID {pid:?}"); 35 | 36 | // Grab last PU and extract its CpuSet 37 | let cpuset = last_pu(&topology)? 38 | .cpuset() 39 | .ok_or_else(|| eyre!("PU objects should have a CpuSet"))?; 40 | 41 | // Query the current cpu binding, and location if supported 42 | let print_binding_location = |situation: &str| -> eyre::Result<()> { 43 | println!( 44 | "Cpu Binding {situation}: {:?}", 45 | topology.process_cpu_binding(pid, CpuBindingFlags::empty())? 46 | ); 47 | if support.get_process_last_cpu_location() { 48 | println!( 49 | "Cpu Location {situation}: {:?}", 50 | topology.last_process_cpu_location(pid, CpuBindingFlags::empty())? 51 | ) 52 | } 53 | Ok(()) 54 | }; 55 | 56 | // Query binding and CPU location before binding 57 | print_binding_location("before binding")?; 58 | 59 | // Bind to one core. 60 | topology.bind_process_cpu(pid, cpuset, CpuBindingFlags::empty())?; 61 | 62 | // Query binding and CPU location after binding 63 | print_binding_location("after binding")?; 64 | 65 | Ok(()) 66 | } 67 | 68 | /// Find the last PU 69 | fn last_pu(topology: &Topology) -> eyre::Result<&TopologyObject> { 70 | topology 71 | .objects_with_type(ObjectType::PU) 72 | .next_back() 73 | .ok_or_else(|| eyre!("system should have at least one PU")) 74 | } 75 | -------------------------------------------------------------------------------- /examples/bind_threads_cpu.rs: -------------------------------------------------------------------------------- 1 | use eyre::eyre; 2 | use hwlocality::{ 3 | cpu::binding::CpuBindingFlags, 4 | object::types::ObjectType, 5 | topology::support::{DiscoverySupport, FeatureSupport}, 6 | Topology, 7 | }; 8 | 9 | /// Example which spawns one thread per core and then assigns it to each. 10 | /// 11 | /// Example Output with 2 cores (no HT) on linux: 12 | /// 13 | /// ``` 14 | /// Found 2 cores. 15 | /// Thread 0: Binding went from 0-1 to 0 16 | /// Thread 1: Binding went from 0-1 to 1 17 | /// ``` 18 | fn main() -> eyre::Result<()> { 19 | // Create topology and check feature support 20 | let topology = Topology::new()?; 21 | if !topology.supports(FeatureSupport::discovery, DiscoverySupport::pu_count) { 22 | println!("This example needs accurate reporting of PU objects"); 23 | return Ok(()); 24 | } 25 | let Some(cpu_support) = topology.feature_support().cpu_binding() else { 26 | println!("This example requires CPU binding support"); 27 | return Ok(()); 28 | }; 29 | if !(cpu_support.get_thread() && cpu_support.set_thread()) { 30 | println!("This example needs support for querying and setting thread CPU bindings"); 31 | return Ok(()); 32 | } 33 | 34 | // Grab the number of cores in a block so that the lock is removed once 35 | // the end of the block is reached. 36 | let core_depth = topology.depth_or_below_for_type(ObjectType::Core)?; 37 | let cores = topology.objects_at_depth(core_depth).collect::>(); 38 | println!("Found {} cores, will bind one thread per core", cores.len()); 39 | 40 | // Spawn one thread for each and pass the topology down into scope. 41 | std::thread::scope(|scope| { 42 | for (idx, core) in cores.into_iter().enumerate() { 43 | let topology = &topology; 44 | scope.spawn(move || -> eyre::Result<()> { 45 | // Get the current thread id and lock the topology to use. 46 | let tid = hwlocality::current_thread_id(); 47 | 48 | // Thread binding before explicit set. 49 | let before = topology.thread_cpu_binding(tid, CpuBindingFlags::empty())?; 50 | 51 | // load the cpuset for the given core index. 52 | let mut bind_to = core 53 | .cpuset() 54 | .ok_or_else(|| eyre!("CPU cores should have CpuSets"))? 55 | .clone_target(); 56 | 57 | // Get only one logical processor (in case the core is SMT/hyper-threaded). 58 | bind_to.singlify(); 59 | 60 | // Set the binding. 61 | topology.bind_thread_cpu(tid, &bind_to, CpuBindingFlags::empty())?; 62 | 63 | // Thread binding after explicit set. 64 | let after = topology.thread_cpu_binding(tid, CpuBindingFlags::empty())?; 65 | 66 | // Report what was done 67 | println!("- Thread {idx} binding: {before:?} -> {after:?}"); 68 | 69 | Ok(()) 70 | }); 71 | } 72 | }); 73 | 74 | Ok(()) 75 | } 76 | -------------------------------------------------------------------------------- /examples/bind_to_last_core.rs: -------------------------------------------------------------------------------- 1 | use eyre::eyre; 2 | use hwlocality::{ 3 | cpu::binding::CpuBindingFlags, 4 | object::{types::ObjectType, TopologyObject}, 5 | Topology, 6 | }; 7 | 8 | /// Bind to the CPU last core of the machine. 9 | /// 10 | /// First find out where cores are, or else smaller sets of logical CPUs if 11 | /// the OS doesn't have the notion of a "core". 12 | /// 13 | /// Example Output with 2 cores (no HT) on linux: 14 | /// 15 | /// ``` 16 | /// Cpu Binding before explicit binding: 0-1 17 | /// Cpu Location before explicit binding: 0 18 | /// Correctly bound to last core 19 | /// Cpu Binding after explicit binding: 1 20 | /// Cpu Location after explicit binding: 1 21 | /// ``` 22 | fn main() -> eyre::Result<()> { 23 | // Create topology and check feature support 24 | let topology = Topology::new()?; 25 | let Some(support) = topology.feature_support().cpu_binding() else { 26 | println!("This example requires CPU binding support"); 27 | return Ok(()); 28 | }; 29 | if !((support.get_current_process() || support.get_current_thread()) 30 | && (support.set_current_process() || support.set_current_thread())) 31 | { 32 | println!( 33 | "This example needs support for querying and setting current process CPU bindings" 34 | ); 35 | return Ok(()); 36 | } 37 | 38 | // Find the last core on the system and extract its CpuSet 39 | let cpuset = last_core(&topology)? 40 | .cpuset() 41 | .ok_or_else(|| eyre!("CPU cores should have CPUsets"))?; 42 | 43 | // Query the current cpu binding, and location if supported 44 | let print_binding_location = |situation: &str| -> eyre::Result<()> { 45 | println!( 46 | "Cpu Binding {situation}: {:?}", 47 | topology.cpu_binding(CpuBindingFlags::ASSUME_SINGLE_THREAD)? 48 | ); 49 | if support.get_current_process_last_cpu_location() { 50 | println!( 51 | "Cpu Location {situation}: {:?}", 52 | topology.last_cpu_location(CpuBindingFlags::ASSUME_SINGLE_THREAD)? 53 | ) 54 | } 55 | Ok(()) 56 | }; 57 | 58 | // Check binding and location before binding 59 | print_binding_location("before explicit binding")?; 60 | 61 | // Try to bind all threads of the current (possibly multithreaded) process. 62 | topology.bind_cpu(cpuset, CpuBindingFlags::ASSUME_SINGLE_THREAD)?; 63 | println!("Correctly bound to last core"); 64 | 65 | // Check binding and location before binding 66 | print_binding_location("after explicit binding")?; 67 | 68 | Ok(()) 69 | } 70 | 71 | /// Find the last core on the system 72 | fn last_core(topology: &Topology) -> eyre::Result<&TopologyObject> { 73 | let core_depth = topology.depth_or_below_for_type(ObjectType::Core)?; 74 | let mut all_cores = topology.objects_at_depth(core_depth); 75 | all_cores 76 | .next_back() 77 | .ok_or_else(|| eyre!("at least one Core or PU should be present")) 78 | } 79 | -------------------------------------------------------------------------------- /examples/number_of_packages.rs: -------------------------------------------------------------------------------- 1 | use hwlocality::{ 2 | object::{depth::TypeToDepthError, types::ObjectType}, 3 | Topology, 4 | }; 5 | 6 | /// Prints the number of packages. 7 | fn main() -> eyre::Result<()> { 8 | let topology = Topology::new()?; 9 | 10 | match topology.depth_for_type(ObjectType::Package) { 11 | Ok(depth) => println!( 12 | "*** Found {} package(s) at depth {depth}", 13 | topology.num_objects_at_depth(depth) 14 | ), 15 | Err(TypeToDepthError::Nonexistent) => println!("*** No package object found"), 16 | Err(TypeToDepthError::Multiple) => println!("*** Package objects exist at multiple depths"), 17 | } 18 | 19 | Ok(()) 20 | } 21 | -------------------------------------------------------------------------------- /examples/process_cpu_bindings.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeMap, BTreeSet}; 2 | 3 | use hwlocality::{ 4 | cpu::{binding::CpuBindingFlags, cpuset::CpuSet}, 5 | topology::{ 6 | builder::BuildFlags, 7 | support::{CpuBindingSupport, FeatureSupport}, 8 | }, 9 | ProcessId, Topology, 10 | }; 11 | use sysinfo::{ProcessRefreshKind, RefreshKind, System}; 12 | 13 | /// Example which displays process CPU bindings 14 | fn main() -> eyre::Result<()> { 15 | // Create topology and check feature support 16 | let topology = Topology::builder() 17 | .with_flags(BuildFlags::INCLUDE_DISALLOWED)? 18 | .build()?; 19 | if !topology.supports(FeatureSupport::cpu_binding, CpuBindingSupport::get_process) { 20 | println!("This example needs support for querying process CPU bindings"); 21 | return Ok(()); 22 | } 23 | if !sysinfo::IS_SUPPORTED_SYSTEM { 24 | println!("This example needs support for querying the process list"); 25 | return Ok(()); 26 | } 27 | 28 | // FIXME: hwloc's get_proc_cpu_binding() mysteriously fails on Windows CI. 29 | // May want to try again once this upstream issue is resolved: 30 | // https://github.com/open-mpi/hwloc/issues/78 31 | if cfg!(target_os = "windows") { 32 | println!( 33 | "This example currently fails on Windows for unknown reasons, and has been disabled" 34 | ); 35 | return Ok(()); 36 | } 37 | 38 | // List processes and group them by CPU binding. 39 | // Use the empty CpuSet as an error sentinel, which is okay since a 40 | // process cannot be bound to no CPU (otherwise it couldn't make progress) 41 | let mut binding_to_pids = BTreeMap::>::new(); 42 | let sys = System::new_with_specifics( 43 | RefreshKind::nothing().with_processes(ProcessRefreshKind::nothing()), 44 | ); 45 | for pid in sys.processes().keys().copied() { 46 | let pid = usize::from(pid) as ProcessId; 47 | let binding = topology 48 | .process_cpu_binding(pid, CpuBindingFlags::empty()) 49 | .unwrap_or(CpuSet::new()); 50 | assert!(binding_to_pids.entry(binding).or_default().insert(pid)); 51 | } 52 | 53 | // Display CPU binding names and PID lists, find the width of the largest display 54 | let mut widest_binding_name = 0; 55 | let mut widest_pid_list = 0; 56 | let displays = binding_to_pids 57 | .into_iter() 58 | .map(|(binding, pid_list)| { 59 | let binding_name = if binding.is_empty() { 60 | "Query failed".to_string() 61 | } else if binding == topology.complete_cpuset() { 62 | "All online CPUs".to_string() 63 | } else if binding == topology.allowed_cpuset() { 64 | "All allowed CPUs".to_string() 65 | } else if binding == topology.cpuset() { 66 | "All visible CPUs".to_string() 67 | } else { 68 | binding.to_string() 69 | }; 70 | let pid_list = format!("{pid_list:?}"); 71 | assert!( 72 | binding_name.is_ascii() && pid_list.is_ascii(), 73 | "Need to use unicode_width if this isn't true" 74 | ); 75 | widest_binding_name = widest_binding_name.max(binding_name.len()); 76 | widest_pid_list = widest_pid_list.max(pid_list.len()); 77 | (binding_name, pid_list) 78 | }) 79 | .collect::>(); 80 | 81 | // Display process PID bindings in a tabular fashion 82 | println!( 83 | "{0:binding_width$} │ PIDs", 84 | "CPU binding", 85 | binding_width = widest_binding_name 86 | ); 87 | println!( 88 | "{0:─ eyre::Result<()> { 10 | let topology = Topology::new()?; 11 | 12 | // Walk caches on an individual PU 13 | let first_pu = topology 14 | .objects_with_type(ObjectType::PU) 15 | .next() 16 | .ok_or_else(|| eyre!("at least one PU should be present"))?; 17 | let (levels, size) = first_pu 18 | .ancestors() 19 | .filter_map(|ancestor| { 20 | if let Some(ObjectAttributes::Cache(cache)) = ancestor.attributes() { 21 | Some(cache.size().expect("Failed to probe cache size").get()) 22 | } else { 23 | None 24 | } 25 | }) 26 | .fold((0, 0), |(levels, total_size), level_size| { 27 | (levels + 1, total_size + level_size) 28 | }); 29 | println!( 30 | "*** Logical processor 0 is covered by {levels} caches totalling {} KiB", 31 | size / 1024 32 | ); 33 | 34 | // Compute aggregate statistics on all available CPU caches 35 | let stats = topology 36 | .cpu_cache_stats() 37 | .ok_or_else(|| eyre!("failed to probe CPU caches"))?; 38 | println!( 39 | "*** System-wide minimal data cache sizes per level: {:?}", 40 | stats.smallest_data_cache_sizes() 41 | ); 42 | println!( 43 | "*** System-wide total data cache size per level: {:?}", 44 | stats.total_data_cache_sizes() 45 | ); 46 | 47 | Ok(()) 48 | } 49 | -------------------------------------------------------------------------------- /examples/support.rs: -------------------------------------------------------------------------------- 1 | use hwlocality::{ 2 | topology::support::{CpuBindingSupport, FeatureSupport, MemoryBindingSupport}, 3 | Topology, 4 | }; 5 | 6 | /// Example on how to check for specific topology support of a feature. 7 | fn main() -> eyre::Result<()> { 8 | let topology = Topology::new()?; 9 | 10 | // Check if processes can be bound to cpusets 11 | println!("CPU binding support:"); 12 | let cpu_binding_support = |check_feature: fn(&CpuBindingSupport) -> bool| { 13 | topology.supports(FeatureSupport::cpu_binding, check_feature) 14 | }; 15 | println!( 16 | "- Current process: {}", 17 | cpu_binding_support(CpuBindingSupport::set_current_process) 18 | ); 19 | println!( 20 | "- Any process: {}", 21 | cpu_binding_support(CpuBindingSupport::set_process) 22 | ); 23 | 24 | // Check if threads can be bound to cpusets 25 | println!( 26 | "- Current thread: {}", 27 | cpu_binding_support(CpuBindingSupport::set_current_thread) 28 | ); 29 | println!( 30 | "- Any thread: {}", 31 | cpu_binding_support(CpuBindingSupport::set_thread) 32 | ); 33 | 34 | // Check if processes can be bound to NUMA nodes 35 | println!("\nMemory binding support:"); 36 | let memory_binding_support = |check_feature: fn(&MemoryBindingSupport) -> bool| { 37 | topology.supports(FeatureSupport::memory_binding, check_feature) 38 | }; 39 | println!( 40 | "- Current process: {}", 41 | memory_binding_support(MemoryBindingSupport::set_current_process) 42 | ); 43 | println!( 44 | "- Any process: {}", 45 | memory_binding_support(MemoryBindingSupport::set_process) 46 | ); 47 | 48 | // Check if threads can be bound to NUMA nodes 49 | println!( 50 | "- Current thread: {}", 51 | memory_binding_support(MemoryBindingSupport::set_current_thread) 52 | ); 53 | 54 | // Check if memory allocations can be bound to NUMA nodes 55 | println!( 56 | "- New bound allocation: {}", 57 | memory_binding_support(MemoryBindingSupport::allocate_bound) 58 | ); 59 | println!( 60 | "- Bind pre-existing allocation: {}", 61 | memory_binding_support(MemoryBindingSupport::set_area) 62 | ); 63 | 64 | // Debug Print all the Support Flags 65 | println!("\nRaw support flags: {:#?}", topology.feature_support()); 66 | 67 | Ok(()) 68 | } 69 | -------------------------------------------------------------------------------- /examples/walk_linear.rs: -------------------------------------------------------------------------------- 1 | use hwlocality::{object::depth::NormalDepth, Topology}; 2 | 3 | /// Walk the topology with an array style, from depth 0 (always Machine) 4 | /// to the lowest depth (always logical processors). 5 | fn main() -> eyre::Result<()> { 6 | let topology = Topology::new()?; 7 | 8 | for depth in NormalDepth::iter_range(NormalDepth::MIN, topology.depth()) { 9 | println!("*** Objects at depth {depth}"); 10 | 11 | for (idx, object) in topology.objects_at_depth(depth).enumerate() { 12 | println!("{idx}: {object}"); 13 | } 14 | } 15 | 16 | Ok(()) 17 | } 18 | -------------------------------------------------------------------------------- /examples/walk_tree.rs: -------------------------------------------------------------------------------- 1 | use hwlocality::{object::TopologyObject, Topology}; 2 | 3 | /// Walk the topologylogy in a tree-style and print it. 4 | fn main() -> eyre::Result<()> { 5 | let topology = Topology::new()?; 6 | 7 | println!("*** Topology tree"); 8 | print_children(topology.root_object(), 0)?; 9 | 10 | Ok(()) 11 | } 12 | 13 | fn print_children(obj: &TopologyObject, depth: usize) -> eyre::Result<()> { 14 | for _ in 0..depth { 15 | print!(" "); 16 | } 17 | print!("{obj}"); 18 | if let Some(os_idx) = obj.os_index() { 19 | print!(" #{os_idx}"); 20 | } 21 | println!(); 22 | 23 | for child in obj.normal_children() { 24 | print_children(child, depth + 1)?; 25 | } 26 | 27 | Ok(()) 28 | } 29 | -------------------------------------------------------------------------------- /hwlocality-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hwlocality-sys" 3 | version = "0.6.1" 4 | authors.workspace = true 5 | edition.workspace = true 6 | rust-version.workspace = true 7 | description = "Low-level bindings for the hwloc hardware locality library" 8 | repository.workspace = true 9 | license.workspace = true 10 | keywords.workspace = true 11 | categories.workspace = true 12 | links = "hwloc" 13 | 14 | # See hwlocality's main Cargo.toml for detailed feature descriptions 15 | [features] 16 | hwloc-latest = ["hwloc-2_12_1"] 17 | hwloc-2_0_4 = [] 18 | hwloc-2_1_0 = ["hwloc-2_0_4"] 19 | hwloc-2_2_0 = ["hwloc-2_1_0"] 20 | hwloc-2_3_0 = ["hwloc-2_2_0"] 21 | hwloc-2_4_0 = ["hwloc-2_3_0"] 22 | hwloc-2_5_0 = ["hwloc-2_4_0"] 23 | hwloc-2_8_0 = ["hwloc-2_5_0"] 24 | hwloc-2_10_0 = ["hwloc-2_8_0"] 25 | hwloc-2_11_0 = ["hwloc-2_10_0"] 26 | hwloc-2_12_0 = ["hwloc-2_11_0"] 27 | hwloc-2_12_1 = ["hwloc-2_12_0"] 28 | hwloc-3_0_0 = ["hwloc-2_12_1"] 29 | vendored = ["dep:autotools", "dep:cmake", "dep:flate2", "dep:sha3", "dep:tar", "dep:ureq"] 30 | vendored-extra = ["vendored"] 31 | # This feature does nothing in -sys and is only here for CI convenience 32 | proptest = [] 33 | 34 | [target.'cfg(windows)'.dependencies] 35 | # Used for OS typedefs 36 | windows-sys.workspace = true 37 | 38 | [target.'cfg(not(windows))'.dependencies] 39 | # Used for OS typedefs 40 | libc.workspace = true 41 | 42 | [build-dependencies] 43 | # Used to locate hwloc except in cmake vendored builds 44 | pkg-config = "0.3.32" 45 | 46 | # Used for vendored builds on all OSes 47 | flate2 = { version = "1.1", optional = true } 48 | sha3 = { version = "0.10.8", optional = true } 49 | tar = { version = "0.4", optional = true } 50 | ureq = { version = "3.1", optional = true } 51 | 52 | # Used for vendored builds targeting OSes other than Windows 53 | autotools = { version = "0.2", optional = true } 54 | 55 | # Used for vendored builds targeting Windows 56 | cmake = { version = "0.1.54", optional = true } 57 | 58 | [dev-dependencies] 59 | # Used to check trait implementations 60 | static_assertions.workspace = true 61 | 62 | [package.metadata.docs.rs] 63 | all-features = true 64 | rustdoc-args = ["--cfg", "docsrs"] 65 | -------------------------------------------------------------------------------- /hwlocality-sys/README.md: -------------------------------------------------------------------------------- 1 | # hwlocality-sys: The low-level bindings below hwlocality 2 | 3 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) 4 | [![On crates.io](https://img.shields.io/crates/v/hwlocality-sys.svg)](https://crates.io/crates/hwlocality-sys) 5 | [![On docs.rs](https://docs.rs/hwlocality-sys/badge.svg)](https://docs.rs/hwlocality-sys/) 6 | [![Continuous Integration](https://img.shields.io/github/actions/workflow/status/HadrienG2/hwlocality/ci.yml?branch=main)](https://github.com/HadrienG2/hwlocality/actions?query=workflow%3A%22Continuous+Integration%22) 7 | [![CII Best Practices Summary](https://img.shields.io/cii/summary/7876)](https://www.bestpractices.dev/en/projects/7876) 8 | ![Requires rustc 1.75.0+](https://img.shields.io/badge/rustc-1.75.0+-lightgray.svg) 9 | 10 | This crate contains the low-level unsafe Rust -> C FFI bindings to 11 | [hwloc](http://www.open-mpi.org/projects/hwloc), that are used to implement the 12 | safe [hwlocality](https://github.com/HadrienG2/hwlocality) bindings. 13 | 14 | Depending on your needs, you can either link to a `libhwloc` that is 15 | pre-installed on your computer or have `hwlocality` build its own copy 16 | `libhwloc` internally. Please read [the "Prerequisites" section of the 17 | hwlocality README](https://github.com/HadrienG2/hwlocality#prerequisites) for 18 | more information about these two options. 19 | 20 | Like any C API, the `hwlocality-sys` low-level bindings are highly unsafe to 21 | use, and it is advised that you use the safe `hwlocality` bindings instead 22 | whenever possible. If you encounter any issue with the safe bindings that 23 | prevents you from using them and forces you to use the unsafe C API directly, 24 | please report them in the issue tracker so we get them fixed. 25 | -------------------------------------------------------------------------------- /hwlocality-sys/build.rs: -------------------------------------------------------------------------------- 1 | use std::sync::OnceLock; 2 | #[cfg(feature = "vendored")] 3 | mod vendored_deps { 4 | pub use flate2::read::GzDecoder; 5 | pub use sha3::{Digest, Sha3_256}; 6 | pub use std::{ 7 | env, 8 | path::{Path, PathBuf}, 9 | }; 10 | pub use tar::Archive; 11 | } 12 | #[cfg(feature = "vendored")] 13 | use vendored_deps::*; 14 | 15 | fn main() { 16 | // We don't need hwloc on docs.rs since it only builds the docs 17 | if std::env::var("DOCS_RS").is_err() { 18 | setup_hwloc(); 19 | } 20 | } 21 | 22 | /// Configure the hwloc dependency 23 | fn setup_hwloc() { 24 | // Determine the minimal supported hwloc version with current features 25 | let required_version = if cfg!(feature = "hwloc-2_12_1") { 26 | "2.12.1" 27 | } else if cfg!(feature = "hwloc-2_12_0") { 28 | "2.12.0" 29 | } else if cfg!(feature = "hwloc-2_11_0") { 30 | "2.11.0" 31 | } else if cfg!(feature = "hwloc-2_10_0") { 32 | "2.10.0" 33 | } else if cfg!(feature = "hwloc-2_8_0") { 34 | "2.8.0" 35 | } else if cfg!(feature = "hwloc-2_5_0") { 36 | "2.5.0" 37 | } else if cfg!(feature = "hwloc-2_4_0") { 38 | "2.4.0" 39 | } else if cfg!(feature = "hwloc-2_3_0") { 40 | "2.3.0" 41 | } else if cfg!(feature = "hwloc-2_2_0") { 42 | "2.2.0" 43 | } else if cfg!(feature = "hwloc-2_1_0") { 44 | "2.1.0" 45 | } else if cfg!(feature = "hwloc-2_0_4") { 46 | "2.0.4" 47 | } else { 48 | "2.0.0" 49 | }; 50 | 51 | // If asked to build hwloc ourselves, do so 52 | #[cfg(feature = "vendored")] 53 | setup_vendored_hwloc(required_version); 54 | 55 | // If asked to use system hwloc, configure it using pkg-config 56 | #[cfg(not(feature = "vendored"))] 57 | find_hwloc(Some(required_version)); 58 | } 59 | 60 | /// Use pkg-config to locate and use a certain hwloc release 61 | fn find_hwloc(required_version: Option<&str>) -> pkg_config::Library { 62 | // Initialize pkg-config 63 | let mut config = pkg_config::Config::new(); 64 | 65 | // Specify the required version range if instructed to do so 66 | if let Some(required_version) = required_version { 67 | let first_unsupported_version = match required_version 68 | .split('.') 69 | .next() 70 | .expect("No major version in required_version") 71 | { 72 | "2" => "3.0.0", 73 | other => panic!("Please add support for hwloc v{other}.x"), 74 | }; 75 | config.range_version(required_version..first_unsupported_version); 76 | } 77 | 78 | // Run pkg-config 79 | let lib = config 80 | .statik(target_os() != "macos") 81 | .probe("hwloc") 82 | .expect("Could not find a suitable version of hwloc"); 83 | 84 | // As it turns-out, pkg-config does not correctly set up the RPATHs for the 85 | // transitive dependencies of hwloc itself in static builds. Fix that. 86 | if target_family().split(',').any(|family| family == "unix") { 87 | for link_path in &lib.link_paths { 88 | println!( 89 | "cargo:rustc-link-arg=-Wl,-rpath,{}", 90 | link_path 91 | .to_str() 92 | .expect("Link path is not an UTF-8 string") 93 | ); 94 | } 95 | } 96 | 97 | // Forward pkg-config output for futher consumption 98 | lib 99 | } 100 | 101 | /// Install hwloc ourselves 102 | #[cfg(feature = "vendored")] 103 | fn setup_vendored_hwloc(required_version: &str) { 104 | // Determine which version to fetch and where to fetch it 105 | let (source_version, sha3_digest) = match required_version 106 | .split('.') 107 | .next() 108 | .expect("No major version in required_version") 109 | { 110 | "2" => ( 111 | "2.12.2", 112 | hex("27052a9696e567968bb84e6252c5bd6277b115745aebd141a79072802b6de578"), 113 | ), 114 | other => panic!("Please add support for bundling hwloc v{other}.x"), 115 | }; 116 | let out_path = env::var("OUT_DIR").expect("No output directory given"); 117 | 118 | // Fetch latest supported hwloc from git 119 | let source_path = fetch_hwloc(out_path, source_version, sha3_digest); 120 | 121 | // On Windows, we build using CMake because the autotools build 122 | // procedure does not work with MSVC, which is often needed on this OS 123 | if target_os() == "windows" { 124 | install_hwloc_cmake(source_path); 125 | } else { 126 | // On other OSes, we use autotools and pkg-config 127 | install_hwloc_autotools(source_path); 128 | } 129 | } 130 | 131 | /// Decode a hexadecimal digest into a stream of bytes 132 | #[cfg(feature = "vendored")] 133 | fn hex(hex: &'static str) -> Box<[u8]> { 134 | assert_eq!(hex.len() % 2, 0, "Digest string {hex:?} should contain full bytes, i.e. pairs of hex digits, but it contains an odd number of bytes"); 135 | hex.as_bytes() 136 | .chunks_exact(2) 137 | .map(|hexdigit_pair| { 138 | hexdigit_pair.iter().copied().fold(0, |acc, mut hexdigit| { 139 | hexdigit = match hexdigit.to_ascii_lowercase() { 140 | b'0'..=b'9' => hexdigit - b'0', 141 | b'a'..=b'f' => hexdigit - b'a' + 10, 142 | _ => { 143 | panic!("Encountered byte that is not a hexadecimal digit in digest string {hex:?}") 144 | } 145 | }; 146 | acc * 16 + hexdigit 147 | }) 148 | }) 149 | .collect() 150 | } 151 | 152 | /// Fetch, check and extract an official hwloc tarball, return extracted path 153 | #[cfg(feature = "vendored")] 154 | fn fetch_hwloc( 155 | parent_path: impl AsRef, 156 | version: &str, 157 | sha3_digest: impl AsRef<[u8]>, 158 | ) -> PathBuf { 159 | // Predict location where tarball would be extracted 160 | let parent_path = parent_path.as_ref(); 161 | let extracted_path = parent_path.join(format!("hwloc-{version}")); 162 | 163 | // Reuse any existing download 164 | if extracted_path.exists() { 165 | eprintln!("Reusing previous hwloc v{version} download"); 166 | return extracted_path; 167 | } 168 | 169 | // Determine hwloc tarball URL 170 | let mut version_components = version.split('.'); 171 | let major = version_components.next().expect("no major hwloc version"); 172 | let minor = version_components.next().expect("no minor hwloc version"); 173 | let url = format!( 174 | "https://download.open-mpi.org/release/hwloc/v{major}.{minor}/hwloc-{version}.tar.gz" 175 | ); 176 | 177 | // Download hwloc tarball 178 | eprintln!("Downloading hwloc v{version} from URL {url}..."); 179 | let tar_gz = ureq::get(url) 180 | .call() 181 | .expect("failed to GET hwloc source") 182 | .into_body() 183 | .read_to_vec() 184 | .expect("failed to parse hwloc source HTTP body"); 185 | 186 | // Verify tarball integrity 187 | eprintln!("Verifying hwloc source integrity..."); 188 | let mut hasher = Sha3_256::new(); 189 | hasher.update(&tar_gz[..]); 190 | assert_eq!( 191 | &hasher.finalize()[..], 192 | sha3_digest.as_ref(), 193 | "downloaded hwloc source failed integrity check" 194 | ); 195 | 196 | // Extract tarball 197 | eprintln!("Extracting hwloc source..."); 198 | let tar = GzDecoder::new(&tar_gz[..]); 199 | let mut archive = Archive::new(tar); 200 | archive 201 | .unpack(parent_path) 202 | .expect("failed to extract hwloc source"); 203 | 204 | // Predict location where tarball was extracted 205 | extracted_path 206 | } 207 | 208 | /// Compile hwloc using cmake, return local installation path 209 | #[cfg(feature = "vendored")] 210 | fn install_hwloc_cmake(source_path: impl AsRef) { 211 | // Locate CMake support files, make sure they are present 212 | // (should be the case on any hwloc release since 2.8) 213 | let cmake_path = source_path.as_ref().join("contrib").join("windows-cmake"); 214 | assert!( 215 | cmake_path.join("CMakeLists.txt").exists(), 216 | "Need hwloc's CMake support to build on Windows (with MSVC)" 217 | ); 218 | 219 | // Configure the CMake build 220 | let mut config = cmake::Config::new(cmake_path); 221 | 222 | // Allow specifying the CMake build profile 223 | if let Ok(profile) = env::var("HWLOC_BUILD_PROFILE") { 224 | config.profile(&profile); 225 | } 226 | 227 | // Allow specifying the build toolchain 228 | if let Ok(toolchain) = env::var("HWLOC_TOOLCHAIN") { 229 | config.define("CMAKE_TOOLCHAIN_FILE", &toolchain); 230 | } 231 | 232 | // Disable testing 233 | config.define("HWLOC_ENABLE_TESTING", "OFF"); 234 | // Disable unnecessary CLI tools 235 | config.define("HWLOC_SKIP_LSTOPO", "1"); 236 | config.define("HWLOC_SKIP_TOOLS", "1"); 237 | 238 | // Set the mode to Release 239 | config.profile("Release"); 240 | 241 | // Build hwloc 242 | let install_path = config.always_configure(false).build(); 243 | 244 | // Configure our own build to use this hwloc 245 | println!("cargo:rustc-link-lib=static=hwloc"); 246 | println!( 247 | "cargo:rustc-link-search={}", 248 | install_path.join("lib").display() 249 | ); 250 | } 251 | 252 | /// Compile hwloc using autotools, return local installation path 253 | #[cfg(feature = "vendored")] 254 | fn install_hwloc_autotools(source_path: impl AsRef) { 255 | // Configure for static linking 256 | let mut config = autotools::Config::new(source_path); 257 | config.enable_static().disable_shared(); 258 | if target_os() == "macos" { 259 | // macOS need some extra stuff to be linked for all symbols to be found 260 | config.ldflag("-F/System/Library/Frameworks -framework CoreFoundation"); 261 | // And libxml2 must be linked in explicitly for some inexplicable reason 262 | if cfg!(feature = "vendored-extra") { 263 | println!("cargo:rustc-link-lib=xml2"); 264 | } 265 | } 266 | 267 | // Disable optional features unless user explicitly asked for them 268 | if cfg!(not(feature = "vendored-extra")) { 269 | config 270 | .disable("cairo", None) 271 | .disable("io", None) 272 | .disable("libxml2", None); 273 | } 274 | 275 | // Run the build 276 | let install_path = config.fast_build(true).reconf("-ivf").build(); 277 | 278 | // Compute the associated PKG_CONFIG_PATH 279 | let new_path = |lib_dir: &str| install_path.join(lib_dir).join("pkgconfig"); 280 | let new_path = format!( 281 | "{}:{}", 282 | new_path("lib").display(), 283 | new_path("lib64").display() 284 | ); 285 | 286 | // Combine it with any pre-existing PKG_CONFIG_PATH 287 | match env::var("PKG_CONFIG_PATH") { 288 | Ok(old_path) if !old_path.is_empty() => { 289 | env::set_var("PKG_CONFIG_PATH", format!("{new_path}:{old_path}")) 290 | } 291 | Ok(_) | Err(env::VarError::NotPresent) => env::set_var("PKG_CONFIG_PATH", new_path), 292 | Err(other_err) => panic!("Failed to check PKG_CONFIG_PATH: {other_err}"), 293 | } 294 | 295 | // Configure this build to use hwloc via pkg-config 296 | find_hwloc(None); 297 | } 298 | 299 | /// Cross-compilation friendly alternative to `cfg!(target_os)` 300 | fn target_os() -> &'static str { 301 | static TARGET_OS: OnceLock> = OnceLock::new(); 302 | TARGET_OS 303 | .get_or_init(|| { 304 | std::env::var("CARGO_CFG_TARGET_OS") 305 | .expect("Cargo should tell us what the target OS is") 306 | .into_boxed_str() 307 | }) 308 | .as_ref() 309 | } 310 | 311 | /// Cross-compilation friendly alternative to `cfg!(target_family)` 312 | fn target_family() -> &'static str { 313 | static TARGET_FAMILY: OnceLock> = OnceLock::new(); 314 | TARGET_FAMILY 315 | .get_or_init(|| { 316 | std::env::var("CARGO_CFG_TARGET_FAMILY") 317 | .expect("Cargo should tell us what the target family is") 318 | .into_boxed_str() 319 | }) 320 | .as_ref() 321 | } 322 | -------------------------------------------------------------------------------- /src/bitmap/cow.rs: -------------------------------------------------------------------------------- 1 | //! Copy-on-write type analogous to [`std::borrow::Cow`] that can either be an 2 | //! owned [`Bitmap`]/[`CpuSet`]/[`NodeSet`] or a [`BitmapRef`] thereof. 3 | 4 | use super::{BitmapRef, OwnedBitmap}; 5 | #[cfg(doc)] 6 | use crate::{bitmap::Bitmap, cpu::cpuset::CpuSet, memory::nodeset::NodeSet}; 7 | #[cfg(doc)] 8 | use std::borrow::Cow; 9 | use std::{ 10 | borrow::Borrow, 11 | fmt::{self, Display, Formatter, Pointer}, 12 | hash::{self, Hash}, 13 | ops::Deref, 14 | }; 15 | 16 | /// Clone-on-write smart pointer to a [`Bitmap`]-like `Target` 17 | /// 18 | /// This is to [`Cow`] what [`BitmapRef`] is to `&Target`. It cannot literally 19 | /// be a standard [`Cow`] because of the same C hwloc API peculiarities that 20 | /// prevent [`BitmapRef`] from being a standard Rust reference. 21 | /// 22 | /// You are not expected to use this type on a regular basis, it only exists to 23 | /// serve the narrow use case of letting you return both owned and borrowed 24 | /// bitmaps from topology-editing callbacks, without having to clone the 25 | /// borrowed bitmaps. This is why its API is less extensive and convenient than 26 | /// that of [`BitmapRef`]. 27 | #[derive(Clone, Debug)] 28 | pub enum BitmapCow<'target, Target: OwnedBitmap> { 29 | /// Borrowed bitmap 30 | Borrowed(BitmapRef<'target, Target>), 31 | 32 | /// Owned bitmap 33 | Owned(Target), 34 | } 35 | 36 | impl<'target, Target: OwnedBitmap> BitmapCow<'target, Target> { 37 | /// Extracts the owned bitmap 38 | /// 39 | /// Clones the bitmap if it is not already owned. 40 | pub fn into_owned(self) -> Target { 41 | match self { 42 | Self::Borrowed(b) => b.clone_target(), 43 | Self::Owned(o) => o, 44 | } 45 | } 46 | } 47 | 48 | impl<'target, Target: OwnedBitmap> AsRef for BitmapCow<'target, Target> { 49 | fn as_ref(&self) -> &Target { 50 | match self { 51 | Self::Borrowed(b) => b.as_ref(), 52 | Self::Owned(o) => o, 53 | } 54 | } 55 | } 56 | 57 | impl Borrow for BitmapCow<'_, Target> { 58 | fn borrow(&self) -> &Target { 59 | self.as_ref() 60 | } 61 | } 62 | 63 | impl Deref for BitmapCow<'_, Target> { 64 | type Target = Target; 65 | 66 | fn deref(&self) -> &Target { 67 | self.as_ref() 68 | } 69 | } 70 | 71 | impl Display for BitmapCow<'_, Target> { 72 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 73 | ::fmt(self.as_ref(), f) 74 | } 75 | } 76 | 77 | impl> Eq for BitmapCow<'_, Target> {} 78 | 79 | impl From for BitmapCow<'_, Target> { 80 | fn from(value: Target) -> Self { 81 | Self::Owned(value) 82 | } 83 | } 84 | 85 | impl<'target, Target: OwnedBitmap> From<&'target Target> for BitmapCow<'target, Target> { 86 | fn from(value: &'target Target) -> Self { 87 | Self::Borrowed(value.into()) 88 | } 89 | } 90 | 91 | impl<'target, Target: OwnedBitmap> From> for BitmapCow<'target, Target> { 92 | fn from(value: BitmapRef<'target, Target>) -> Self { 93 | Self::Borrowed(value) 94 | } 95 | } 96 | 97 | impl<'target, Target: OwnedBitmap + Hash> Hash for BitmapCow<'target, Target> { 98 | fn hash(&self, state: &mut H) { 99 | self.as_ref().hash(state) 100 | } 101 | } 102 | 103 | impl PartialEq for BitmapCow<'_, Target> 104 | where 105 | Target: OwnedBitmap + PartialEq, 106 | { 107 | fn eq(&self, other: &Rhs) -> bool { 108 | self.as_ref() == other 109 | } 110 | } 111 | 112 | impl Pointer for BitmapCow<'_, Target> { 113 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 114 | ::fmt(self.as_ref(), f) 115 | } 116 | } 117 | 118 | #[cfg(test)] 119 | mod tests { 120 | use super::*; 121 | use crate::{ 122 | bitmap::{Bitmap, SpecializedBitmap}, 123 | strategies::topology_related_set, 124 | topology::Topology, 125 | }; 126 | use proptest::prelude::*; 127 | #[allow(unused)] 128 | use similar_asserts::assert_eq; 129 | use std::{collections::hash_map::RandomState, hash::BuildHasher}; 130 | 131 | // Test BitmapCow construction and intrinsic properties 132 | proptest! { 133 | /// Test construction from cpuset 134 | #[test] 135 | fn from_cpuset(cpuset in topology_related_set(Topology::complete_cpuset)) { 136 | check_specialized_cow(cpuset)?; 137 | } 138 | 139 | /// Test construction from nodeset 140 | #[test] 141 | fn from_nodeset(nodeset in topology_related_set(Topology::complete_nodeset)) { 142 | check_specialized_cow(nodeset)?; 143 | } 144 | } 145 | 146 | /// Test construction of [`BitmapCow`] from specialized bitmaps 147 | fn check_specialized_cow(target: impl SpecializedBitmap) -> Result<(), TestCaseError> { 148 | check_any_cow(target.clone())?; 149 | check_any_cow::(target.into())?; 150 | Ok(()) 151 | } 152 | 153 | /// Test construction of [`BitmapCow`] from any bitmap 154 | fn check_any_cow(target: Target) -> Result<(), TestCaseError> { 155 | { 156 | let from_owned = BitmapCow::from(target.clone()); 157 | prop_assert!(matches!( 158 | &from_owned, 159 | BitmapCow::Owned(target2) if *target2 == target 160 | )); 161 | check_cow(from_owned)?; 162 | } 163 | { 164 | let from_rust_ref = BitmapCow::from(&target); 165 | prop_assert!(matches!( 166 | &from_rust_ref, 167 | BitmapCow::Borrowed(target2) if *target2 == target 168 | )); 169 | check_cow(from_rust_ref)?; 170 | } 171 | { 172 | let from_bitmap_ref = BitmapCow::from(BitmapRef::from(&target)); 173 | prop_assert!(matches!( 174 | &from_bitmap_ref, 175 | BitmapCow::Borrowed(target2) if *target2 == target 176 | )); 177 | check_cow(from_bitmap_ref)?; 178 | } 179 | Ok(()) 180 | } 181 | 182 | /// Test properties of a [`BitmapCow`] 183 | fn check_cow(cow: BitmapCow<'_, Target>) -> Result<(), TestCaseError> { 184 | let owned: Target = cow.clone().into_owned(); 185 | prop_assert_eq!(&cow, &owned); 186 | 187 | let mut target_ref: &Target = cow.as_ref(); 188 | prop_assert_eq!(target_ref, &owned); 189 | 190 | target_ref = cow.borrow(); 191 | prop_assert_eq!(target_ref, &owned); 192 | 193 | target_ref = &cow; 194 | prop_assert_eq!(target_ref, &owned); 195 | 196 | prop_assert_eq!(cow.to_string(), owned.to_string()); 197 | 198 | let bh = RandomState::new(); 199 | prop_assert_eq!(bh.hash_one(&cow), bh.hash_one(&owned)); 200 | 201 | prop_assert_eq!(format!("{:p}", *target_ref), format!("{cow:p}")); 202 | prop_assert_ne!(format!("{owned:p}"), format!("{cow:p}")); 203 | Ok(()) 204 | } 205 | 206 | // Test properties of pair of BitmapCows 207 | proptest! { 208 | /// Test construction from cpuset 209 | #[test] 210 | fn cpuset_pair( 211 | cpusets in prop::array::uniform2(topology_related_set(Topology::complete_cpuset)) 212 | ) { 213 | check_specialized_cow_pair(cpusets)?; 214 | } 215 | 216 | /// Test construction from nodeset 217 | #[test] 218 | fn nodeset_pair( 219 | nodesets in prop::array::uniform2(topology_related_set(Topology::complete_nodeset)) 220 | ) { 221 | check_specialized_cow_pair(nodesets)?; 222 | } 223 | } 224 | 225 | /// Test construction of [`BitmapCow`] from specialized bitmaps 226 | fn check_specialized_cow_pair( 227 | targets: [impl SpecializedBitmap; 2], 228 | ) -> Result<(), TestCaseError> { 229 | check_any_cow_pair(targets.clone())?; 230 | check_any_cow_pair::(targets.map(Into::into))?; 231 | Ok(()) 232 | } 233 | 234 | /// Test construction of [`BitmapCow`] from any bitmap 235 | fn check_any_cow_pair(targets: [Target; 2]) -> Result<(), TestCaseError> { 236 | check_cow_pair(targets.clone().map(BitmapCow::from))?; 237 | let [target1_ref, target2_ref] = &targets; 238 | let target_refs = [target1_ref, target2_ref]; 239 | check_cow_pair(target_refs.map(BitmapCow::from))?; 240 | Ok(()) 241 | } 242 | 243 | /// Test properties of a [`BitmapCow`] 244 | fn check_cow_pair( 245 | [cow1, cow2]: [BitmapCow<'_, Target>; 2], 246 | ) -> Result<(), TestCaseError> { 247 | let target1: &Target = &cow1; 248 | let target2: &Target = &cow2; 249 | prop_assert_eq!(cow1 == *target2, target1 == target2); 250 | Ok(()) 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/cpu/cache.rs: -------------------------------------------------------------------------------- 1 | //! CPU cache statistics 2 | //! 3 | //! These statistics, which can be queried via 4 | //! the [`Topology::cpu_cache_stats()`] method, can be used to perform simple 5 | //! cache locality optimizations when your performance requirements do not call 6 | //! for full locality-aware scheduling with manual task and memory pinning. 7 | //! 8 | //! This functionality is an hwlocality-specific extension to the hwloc API. 9 | 10 | use crate::{ 11 | object::{attributes::ObjectAttributes, types::ObjectType}, 12 | topology::Topology, 13 | }; 14 | use arrayvec::ArrayVec; 15 | #[allow(unused)] 16 | #[cfg(test)] 17 | use similar_asserts::assert_eq; 18 | 19 | /// # CPU cache statistics 20 | impl Topology { 21 | /// Compute CPU cache statistics, if cache sizes are known 22 | /// 23 | /// Returns None if cache size information is unavailable for at least some 24 | /// of the CPU caches on the system. 25 | /// 26 | /// These statistics can be used to perform simple cache locality 27 | /// optimizations when your performance requirements do not call for full 28 | /// locality-aware scheduling with careful task and memory pinning. 29 | /// 30 | /// This functionality is an hwlocality-specific extension to the hwloc API. 31 | /// 32 | /// # Examples 33 | /// 34 | /// ``` 35 | /// # let topology = hwlocality::Topology::test_instance(); 36 | /// # 37 | /// use eyre::eyre; 38 | /// 39 | /// let stats = topology 40 | /// .cpu_cache_stats() 41 | /// .ok_or_else(|| eyre!("CPU cache sizes are not known"))?; 42 | /// 43 | /// println!( 44 | /// "Minimal data cache sizes: {:?}", 45 | /// stats.smallest_data_cache_sizes() 46 | /// ); 47 | /// println!( 48 | /// "Minimal data cache sizes per thread: {:?}", 49 | /// stats.smallest_data_cache_sizes_per_thread() 50 | /// ); 51 | /// println!( 52 | /// "Total data cache sizes: {:?}", 53 | /// stats.total_data_cache_sizes() 54 | /// ); 55 | /// # 56 | /// # Ok::<(), eyre::Report>(()) 57 | /// ``` 58 | pub fn cpu_cache_stats(&self) -> Option { 59 | CpuCacheStats::new(self) 60 | } 61 | } 62 | 63 | /// CPU cache statistics 64 | /// 65 | /// These statistics can be used to perform simple cache locality optimizations 66 | /// when your performance requirements do not call for full locality-aware 67 | /// scheduling with manual task and memory pinning. 68 | // 69 | // --- Implementation notes --- 70 | // 71 | // Not implementing Copy to leave room for future growth in case people really 72 | // insist that I use a Vec instead of an ArrayVec someday. 73 | #[derive(Clone, Debug, Eq, Hash, PartialEq)] 74 | pub struct CpuCacheStats { 75 | /// Size of the smallest caches of each type 76 | smallest_data_cache_sizes: ArrayVec, 77 | 78 | /// Size per thread of the smallest caches of each type 79 | smallest_data_cache_sizes_per_thread: ArrayVec, 80 | 81 | /// Sum of the sizes of the caches of each type 82 | total_data_cache_sizes: ArrayVec, 83 | } 84 | 85 | impl CpuCacheStats { 86 | /// Compute CPU cache statistics, if cache sizes are known 87 | /// 88 | /// Returns None if cache size information is unavailable for at least some 89 | /// of the CPU caches on the system. 90 | pub fn new(topology: &Topology) -> Option { 91 | let mut smallest_data_cache_sizes = ArrayVec::new(); 92 | let mut smallest_data_cache_sizes_per_thread = ArrayVec::new(); 93 | let mut total_data_cache_sizes = ArrayVec::new(); 94 | for &data_cache_level in DATA_CACHE_LEVELS { 95 | // Compute cache capacity stats for this cache level 96 | let mut smallest = u64::MAX; 97 | let mut smallest_per_thread = u64::MAX; 98 | let mut total = 0; 99 | let mut found = false; 100 | for object in topology.objects_with_type(data_cache_level) { 101 | let Some(ObjectAttributes::Cache(cache)) = object.attributes() else { 102 | unreachable!("Caches should have cache attributes") 103 | }; 104 | found = true; 105 | let num_threads = object 106 | .cpuset() 107 | .and_then(|set| set.weight()) 108 | .expect("Caches should have cpusets") as u64; 109 | let cache_size = cache.size()?.get(); 110 | let per_thread_size = cache_size / num_threads; 111 | smallest = smallest.min(cache_size); 112 | smallest_per_thread = smallest_per_thread.min(per_thread_size); 113 | total += cache_size; 114 | } 115 | 116 | // If at least one cache was found, record stats, otherwise abort: 117 | // If we didn't find cache level N, we won't find level N+1. 118 | if found { 119 | smallest_data_cache_sizes.push(smallest); 120 | smallest_data_cache_sizes_per_thread.push(smallest_per_thread); 121 | total_data_cache_sizes.push(total); 122 | } else { 123 | break; 124 | } 125 | } 126 | 127 | // Absence of L1 caches is not possible on current hardware, it 128 | // is just interpreted as cache info being unavailable overall 129 | (!smallest_data_cache_sizes.is_empty()).then_some(Self { 130 | smallest_data_cache_sizes, 131 | smallest_data_cache_sizes_per_thread, 132 | total_data_cache_sizes, 133 | }) 134 | } 135 | 136 | /// Smallest CPU data cache capacity at each cache level 137 | /// 138 | /// This tells you how many cache levels there are in the deepest cache 139 | /// hierarchy on this system, and what is the minimal 140 | /// cache capacity at each level. 141 | /// 142 | /// You should tune sequential algorithms such that they fit this effective 143 | /// cache hierarchy (first layer of loop blocking has a working set that can 144 | /// stay in the first reported cache capacity, second layer of loop blocking 145 | /// has a working set that can fit in the second reported capacity, etc.) 146 | pub fn smallest_data_cache_sizes(&self) -> &[u64] { 147 | &self.smallest_data_cache_sizes[..] 148 | } 149 | 150 | /// Smallest CPU data cache capacity at each cache level, per thread 151 | /// 152 | /// This tells you how many cache levels there are in the deepest cache 153 | /// hierarchy on this system, and what is the minimal cache capacity _per 154 | /// thread sharing a cache_ at each level. 155 | /// 156 | /// In parallel algorithms where all CPU threads are potentially used, and 157 | /// threads effectively share no common data, you should tune the private 158 | /// working set of each thread such that it fits this effective cache 159 | /// hierarchy (first layer of loop blocking has a working set that can stay 160 | /// in the first reported cache capacity, second layer of loop blocking has 161 | /// a working set that can fit in the second reported capacity, etc.). 162 | pub fn smallest_data_cache_sizes_per_thread(&self) -> &[u64] { 163 | &self.smallest_data_cache_sizes_per_thread[..] 164 | } 165 | 166 | /// Total CPU data cache capacity at each cache level 167 | /// 168 | /// This tells you how many cache levels there are in the deepest cache 169 | /// hierarchy on this system, and what is the total cache capacity at each 170 | /// level. 171 | /// 172 | /// You should tune parallel algorithms such that the total working set 173 | /// (summed across all threads without double-counting shared resources) 174 | /// fits in the reported aggregated cache capacities. 175 | /// 176 | /// Beware that this is only a minimal requirement for cache locality, and 177 | /// programs honoring this criterion might still not achieve good cache 178 | /// performance due to CPU core heterogeneity or Non-Uniform Cache Access 179 | /// (NUCA) effects. To correctly handle these, you need to move to a fully 180 | /// locality-aware design with threads pinned to CPU cores and tree-like 181 | /// synchronization following the shape of the topology tree. 182 | /// 183 | /// That being said, you may manage to reduce NUCA effects at the cost of 184 | /// using a smaller fraction of your CPU cache capacity by making your 185 | /// parallel algorithm collectively fit into the smallest last-level cache. 186 | pub fn total_data_cache_sizes(&self) -> &[u64] { 187 | &self.total_data_cache_sizes[..] 188 | } 189 | } 190 | 191 | /// Data (or unified) caches levels supported by hwloc 192 | const DATA_CACHE_LEVELS: &[ObjectType] = &[ 193 | ObjectType::L1Cache, 194 | ObjectType::L2Cache, 195 | ObjectType::L3Cache, 196 | ObjectType::L4Cache, 197 | ObjectType::L5Cache, 198 | ]; 199 | 200 | #[cfg(test)] 201 | mod tests { 202 | use super::*; 203 | use similar_asserts::assert_eq; 204 | 205 | #[test] 206 | fn stats() { 207 | let topology = Topology::test_instance(); 208 | let stats = topology.cpu_cache_stats(); 209 | 210 | if let Some(stats) = stats { 211 | // All stats should cover the same number of cache levels 212 | let num_cache_levels = stats.smallest_data_cache_sizes().len(); 213 | assert_eq!( 214 | num_cache_levels, 215 | stats.smallest_data_cache_sizes_per_thread().len() 216 | ); 217 | assert_eq!(num_cache_levels, stats.total_data_cache_sizes().len()); 218 | 219 | // Number of cache levels shouldn't exceed hwloc's ability 220 | assert!(num_cache_levels <= DATA_CACHE_LEVELS.len()); 221 | 222 | // Now let's look at individual cache levels 223 | for (idx, cache_level) in DATA_CACHE_LEVELS.iter().copied().enumerate() { 224 | // Should only report nothing for levels with no known cache 225 | if idx >= num_cache_levels { 226 | assert_eq!(topology.objects_with_type(cache_level).count(), 0); 227 | continue; 228 | } 229 | 230 | // Stats should be accurate 231 | let cache_sizes_and_arities = topology.objects_with_type(cache_level).map(|obj| { 232 | let arity = obj.cpuset().unwrap().weight().unwrap(); 233 | let Some(ObjectAttributes::Cache(cache)) = obj.attributes() else { 234 | unreachable!() 235 | }; 236 | (cache.size().unwrap().get(), arity) 237 | }); 238 | assert_eq!( 239 | stats.smallest_data_cache_sizes()[idx], 240 | cache_sizes_and_arities 241 | .clone() 242 | .map(|(size, _)| size) 243 | .min() 244 | .unwrap() 245 | ); 246 | assert_eq!( 247 | stats.smallest_data_cache_sizes_per_thread()[idx], 248 | cache_sizes_and_arities 249 | .clone() 250 | .map(|(size, arity)| size / arity as u64) 251 | .min() 252 | .unwrap() 253 | ); 254 | assert_eq!( 255 | stats.total_data_cache_sizes()[idx], 256 | cache_sizes_and_arities 257 | .clone() 258 | .map(|(size, _)| size) 259 | .sum::() 260 | ); 261 | } 262 | } else { 263 | // Cache stats queries should only fail if hwloc failed to probe CPU 264 | // cache properties, overall or cache size specifically. 265 | if topology.objects_with_type(ObjectType::L1Cache).count() == 0 { 266 | return; 267 | } 268 | for &cache_level in DATA_CACHE_LEVELS { 269 | for obj in topology.objects_with_type(cache_level) { 270 | let Some(ObjectAttributes::Cache(cache)) = obj.attributes() else { 271 | unreachable!() 272 | }; 273 | if cache.size().is_none() { 274 | return; 275 | } 276 | } 277 | } 278 | panic!("Cache stats query should not have failed"); 279 | } 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/cpu/mod.rs: -------------------------------------------------------------------------------- 1 | //! CPU-specific functionality 2 | //! 3 | //! Most of this module's functionality is exposed via methods of the 4 | //! [`Topology`] struct. The module itself only hosts type definitions that are 5 | //! related to this functionality. 6 | 7 | pub mod binding; 8 | pub mod cache; 9 | pub mod cpuset; 10 | #[cfg(feature = "hwloc-2_4_0")] 11 | pub mod kind; 12 | 13 | #[cfg(doc)] 14 | use crate::topology::Topology; 15 | -------------------------------------------------------------------------------- /src/ffi/mod.rs: -------------------------------------------------------------------------------- 1 | //! Useful tools for interacting with the hwloc C API 2 | //! 3 | //! This module contains a bunch of small utilities that proved convenient when 4 | //! interacting with hwloc entry points, but do not clearly belong in any other 5 | //! module. 6 | 7 | pub(crate) mod int; 8 | pub(crate) mod string; 9 | pub(crate) mod transparent; 10 | pub(crate) mod unknown; 11 | 12 | #[allow(unused)] 13 | #[cfg(test)] 14 | use similar_asserts::assert_eq; 15 | use std::{ 16 | ffi::{c_char, CStr}, 17 | fmt, ptr, 18 | }; 19 | 20 | // Re-export the PositiveInt and UnknownVariant types 21 | pub use self::{int::PositiveInt, unknown::UnknownVariant}; 22 | 23 | /// Dereference a C pointer with correct lifetime (*const -> & version) 24 | /// 25 | /// # Safety 26 | /// 27 | /// If non-null, p must be safe to dereference for the duration of the 28 | /// reference-to-pointer's lifetime, and the target data must not be modified 29 | /// as long as the reference exists. 30 | pub(crate) unsafe fn deref_ptr(p: &*const T) -> Option<&T> { 31 | if p.is_null() { 32 | return None; 33 | } 34 | // SAFETY: Per input precondition 35 | Some(unsafe { &**p }) 36 | } 37 | 38 | /// Dereference a C pointer with correct lifetime (*mut -> & version) 39 | /// 40 | /// # Safety 41 | /// 42 | /// If non-null, p must be safe to dereference for the duration of the 43 | /// reference-to-pointer's lifetime, and the target data must not be modified 44 | /// as long as the reference exists. 45 | pub(crate) unsafe fn deref_ptr_mut(p: &*mut T) -> Option<&T> { 46 | if p.is_null() { 47 | return None; 48 | } 49 | // SAFETY: Per input precondition 50 | Some(unsafe { &**p }) 51 | } 52 | 53 | /// Dereference a C pointer with correct lifetime (*mut -> &mut version) 54 | /// 55 | /// # Safety 56 | /// 57 | /// If non-null, p must be safe to dereference for the duration of the 58 | /// reference-to-pointer's lifetime, and the target data must only be modified 59 | /// via the output reference and not be read via any other channel as long as 60 | /// the reference exists. 61 | #[allow(unused)] 62 | pub(crate) unsafe fn deref_mut_ptr(p: &mut *mut T) -> Option<&mut T> { 63 | if p.is_null() { 64 | return None; 65 | } 66 | // SAFETY: Per input precondition 67 | Some(unsafe { &mut **p }) 68 | } 69 | 70 | /// Dereference a C-style string with correct lifetime 71 | /// 72 | /// # Safety 73 | /// 74 | /// If non-null, p must be safe to dereference for the duration of the 75 | /// reference-to-pointer's lifetime, and the target data must not be modified 76 | /// as long as the reference exists. 77 | pub(crate) unsafe fn deref_str(p: &*mut c_char) -> Option<&CStr> { 78 | if p.is_null() { 79 | return None; 80 | } 81 | // SAFETY: Per input precondition 82 | Some(unsafe { CStr::from_ptr(*p) }) 83 | } 84 | 85 | /// Get text output from an snprintf-like function 86 | /// 87 | /// # Safety 88 | /// 89 | /// `snprintf` must behave like the libc `snprintf()` function: 90 | /// 91 | /// - If called with a null pointer and a zero length, it should return the 92 | /// length of the output string (without trailing zero). 93 | /// - If called with a pointer to a buffer and the length of that buffer, it 94 | /// should write text to the buffer (with a trailing zero) and not affect it 95 | /// in any other way. 96 | pub(crate) unsafe fn call_snprintf( 97 | mut snprintf: impl FnMut(*mut c_char, usize) -> i32, 98 | ) -> Box<[c_char]> { 99 | /// Polymorphized version of this function (avoids generics code bloat) 100 | fn polymorphized(snprintf: &mut dyn FnMut(*mut c_char, usize) -> i32) -> Box<[c_char]> { 101 | // SAFETY: Per input precondition 102 | let len_i32 = snprintf(ptr::null_mut(), 0); 103 | let len = 104 | usize::try_from(len_i32).expect("Got invalid string length from an snprintf-like API"); 105 | let mut buf = vec![0; len + 1]; 106 | #[cfg(not(tarpaulin_include))] 107 | assert_eq!( 108 | // SAFETY: Per input precondition 109 | snprintf(buf.as_mut_ptr(), buf.len()), 110 | len_i32, 111 | "Got inconsistent string length from an snprintf-like API" 112 | ); 113 | #[cfg(not(tarpaulin_include))] 114 | assert_eq!( 115 | buf.last().copied(), 116 | Some(0), 117 | "Got non-NUL char at end of snprintf-like API output" 118 | ); 119 | buf.into() 120 | } 121 | polymorphized(&mut snprintf) 122 | } 123 | 124 | /// Send the output of an snprintf-like function to a standard Rust formatter 125 | /// 126 | /// # Safety 127 | /// 128 | /// Same as [`call_snprintf()`]. 129 | pub(crate) unsafe fn write_snprintf( 130 | f: &mut fmt::Formatter<'_>, 131 | mut snprintf: impl FnMut(*mut c_char, usize) -> i32, 132 | ) -> fmt::Result { 133 | /// Polymorphized version of this function (avoids generics code bloat) 134 | fn polymorphized( 135 | f: &mut fmt::Formatter<'_>, 136 | snprintf: &mut dyn FnMut(*mut c_char, usize) -> i32, 137 | ) -> fmt::Result { 138 | // SAFETY: Per input precondition 139 | let buf = unsafe { call_snprintf(snprintf) }; 140 | // SAFETY: - The memory comes from a Box<[c_char]> ending with a 141 | // NUL terminator. 142 | // - The original buf is not modified or deallocated as long as the 143 | // text CStr pointing to it is live. 144 | let text = unsafe { CStr::from_ptr(buf.as_ptr()) }.to_string_lossy(); 145 | f.pad(&text) 146 | } 147 | polymorphized(f, &mut snprintf) 148 | } 149 | 150 | #[cfg(test)] 151 | mod tests { 152 | use super::*; 153 | #[allow(unused)] 154 | use similar_asserts::assert_eq; 155 | use std::{ffi::CString, fmt::Debug}; 156 | 157 | #[test] 158 | fn deref_ptr_like() { 159 | // SAFETY: Safe to call on a null pointer 160 | assert_eq!(unsafe { deref_ptr(&ptr::null::()) }, None); 161 | // SAFETY: Safe to call on a null pointer 162 | assert_eq!(unsafe { deref_ptr_mut(&ptr::null_mut::()) }, None); 163 | // SAFETY: Safe to call on a null pointer 164 | assert_eq!(unsafe { deref_mut_ptr(&mut ptr::null_mut::()) }, None); 165 | 166 | let mut x = 42; 167 | { 168 | let p: *const u32 = &x; 169 | // SAFETY: Valid because the &x that p is from was valid 170 | let r = unsafe { deref_ptr(&p).unwrap() }; 171 | assert!(ptr::eq(r, &x)); 172 | } 173 | { 174 | let p_mut: *mut u32 = &mut x; 175 | // SAFETY: Valid because the &mut x that p is from was valid and the 176 | // original &mut reference has gone out of scope 177 | let r = unsafe { deref_ptr_mut(&p_mut).unwrap() }; 178 | assert!(ptr::eq(r, &x)); 179 | } 180 | { 181 | let mut p_mut: *mut u32 = &mut x; 182 | // SAFETY: Valid because the &mut x that p is from was valid and the 183 | // original &mut reference has gone out of scope 184 | let r = unsafe { deref_mut_ptr(&mut p_mut).unwrap() }; 185 | assert!(ptr::eq(r, &x)); 186 | } 187 | } 188 | 189 | #[test] 190 | fn deref_str() { 191 | assert_eq!( 192 | // SAFETY: Safe to call on a null pointer 193 | unsafe { super::deref_str(&ptr::null_mut::()) }, 194 | None 195 | ); 196 | 197 | let s = CString::new("Hello world").unwrap(); 198 | let p = s.as_ptr().cast_mut(); 199 | // SAFETY: This is just a very convoluted way of borrowing s 200 | let s2 = unsafe { super::deref_str(&p).unwrap() }; 201 | assert!(ptr::eq(s2.as_ptr(), p.cast_const())); 202 | } 203 | 204 | #[test] 205 | fn snprintf_wrappers() { 206 | const ORIGINAL: &str = "I'm sorry, Dave\0"; 207 | 208 | // # Safety 209 | // 210 | // Must be called with correct snprintf inputs 211 | unsafe fn snprintf(buf: *mut c_char, len: usize) -> i32 { 212 | assert!(!(buf.is_null() && len != 0)); 213 | isize::try_from(len).expect("Allocation is too large"); 214 | let original_bytes = ORIGINAL.as_bytes(); 215 | if !buf.is_null() { 216 | let truncated_len = len.min(original_bytes.len()); 217 | // SAFETY: Acceptable per input assumption 218 | let buf = unsafe { std::slice::from_raw_parts_mut(buf.cast::(), len) }; 219 | buf.copy_from_slice(&original_bytes[..truncated_len]); 220 | } 221 | i32::try_from(original_bytes.len() - 1).unwrap() 222 | } 223 | 224 | // SAFETY: snprintf closure is indeed an snprintf-like function 225 | assert!(unsafe { call_snprintf(|buf, len| snprintf(buf, len)) } 226 | .iter() 227 | .copied() 228 | .eq(ORIGINAL.bytes().map(|b| c_char::try_from(b).unwrap()))); 229 | 230 | struct Harness; 231 | // 232 | impl Debug for Harness { 233 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 234 | // SAFETY: - write_snprintf can handle snprintf correctly 235 | // - snprintf honors write_snprintf's assumptions 236 | unsafe { write_snprintf(f, |buf, len| snprintf(buf, len)) } 237 | } 238 | } 239 | // 240 | let x = format!("{Harness:?}"); 241 | assert_eq!(x, &ORIGINAL[..ORIGINAL.len() - 1]); 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/ffi/string.rs: -------------------------------------------------------------------------------- 1 | //! A less error-prone [`CString`] alternative 2 | 3 | use crate::errors::NulError; 4 | #[cfg(any(test, feature = "proptest"))] 5 | use proptest::prelude::*; 6 | #[allow(unused)] 7 | #[cfg(test)] 8 | use similar_asserts::assert_eq; 9 | use std::{ 10 | ffi::{c_char, c_void}, 11 | fmt::{self, Debug, Display}, 12 | ptr::NonNull, 13 | }; 14 | 15 | /// Less error-prone [`CString`] alternative 16 | /// 17 | /// This fulfills a subset of the goals of [`CString`] (go from Rust [`&str`] to 18 | /// C `*char`) but with... 19 | /// 20 | /// - Contents that are guaranteed to be UTF-8 21 | /// - A less error-prone borrowing API 22 | /// - A libc-backed allocation whose ownership can safely be transferred to C 23 | /// libraries that manage memory using malloc/free like hwloc, as long as this 24 | /// program is linked to the same libc. 25 | // 26 | // --- Implementation details 27 | // 28 | // # Safety 29 | // 30 | // As a type invariant, the inner pointer is assumed to always point to a valid, 31 | // non-aliased C string. 32 | pub(crate) struct LibcString(NonNull<[c_char]>); 33 | 34 | impl LibcString { 35 | /// Convert a Rust string to a C-compatible representation 36 | /// 37 | /// Returns `None` if the Rust string cannot be converted to a C 38 | /// representation because it contains null chars. 39 | pub(crate) fn new(s: impl AsRef) -> Result { 40 | /// Polymorphized version of this function (avoids generics code bloat) 41 | fn polymorphized(s: &str) -> Result { 42 | // Check input string for inner null chars 43 | if s.contains('\0') { 44 | return Err(NulError); 45 | } 46 | 47 | // Make sure the output C string would follow Rust's rules 48 | let len = s.len() + 1; 49 | #[cfg(not(tarpaulin_include))] 50 | assert!( 51 | len < isize::MAX as usize, 52 | "Cannot add a final NUL without breaking Rust's slice requirements" 53 | ); 54 | 55 | // Allocate C string and wrap it in Self for auto-deallocation 56 | // SAFETY: malloc is safe to call for any nonzero length 57 | let buf = unsafe { libc::malloc(len) }.cast::(); 58 | let buf = NonNull::new(buf).expect("Failed to allocate string buffer"); 59 | // Eventually using this slice pointer is safe because... 60 | // - buf is valid for reads and writes for len bytes 61 | // - Byte pointers are always aligned 62 | // - buf will be initialized and unaliased by the time this pointer is 63 | // dereferenced 64 | // - len was checked to fit Rust's slice size requirements 65 | let buf = NonNull::slice_from_raw_parts(buf, len); 66 | let result = LibcString(buf); 67 | 68 | // Fill the string and return it 69 | let start = buf.as_ptr().cast::(); 70 | // SAFETY: - By definition of a slice, the source range is correct 71 | // - It is OK to write s.len() bytes as buf is of len s.len() + 1 72 | // - Byte pointers are always aligned 73 | // - Overlap between unrelated memory allocations is impossible 74 | unsafe { start.copy_from_nonoverlapping(s.as_ptr(), s.len()) }; 75 | // SAFETY: Index s.len() is in bounds in a buffer of length s.len() + 1 76 | unsafe { start.add(s.len()).write(b'\0') }; 77 | Ok(result) 78 | } 79 | polymorphized(s.as_ref()) 80 | } 81 | 82 | /// Query the Rust version of this C string 83 | pub(crate) fn as_str(&self) -> &str { 84 | // SAFETY: Per type invariant, this is a C string composed of an &str 85 | // followed by a terminating NUL. 86 | unsafe { 87 | std::str::from_utf8_unchecked(std::slice::from_raw_parts( 88 | self.borrow().cast::(), 89 | self.len() - 1, 90 | )) 91 | } 92 | } 93 | 94 | /// Check the length of the string, including NUL terminator 95 | pub(crate) fn len(&self) -> usize { 96 | self.0.len() 97 | } 98 | 99 | /// Make the string momentarily available to a C API that expects `const char*` 100 | /// 101 | /// Make sure the C API does not retain any pointer to the string after 102 | /// this [`LibcString`] is deallocated! 103 | pub(crate) fn borrow(&self) -> *const c_char { 104 | self.0.as_ptr().cast::() 105 | } 106 | 107 | /// Transfer ownership of the string to a C API 108 | /// 109 | /// # Safety 110 | /// 111 | /// Unlike with regular [`CString`], it is safe to pass this string to a C 112 | /// API that may later free it using `free()`, **as long as the application 113 | /// links against the same libc/CRT as hwloc**. 114 | /// 115 | /// However, this last constraint is very hard to enforce in practice on 116 | /// Windows, where the use of multiple CRT is the norm, libraries are 117 | /// commonly distributed as binary DLLs, and you have no easy way to tell 118 | /// which CRT the DLL you're using links against. This is why this method is 119 | /// unsafe, and should not be exposed via a safe interface on Windows. 120 | #[cfg(any(test, feature = "hwloc-2_3_0"))] 121 | pub(crate) unsafe fn into_raw(self) -> *mut c_char { 122 | let ptr = self.0.as_ptr().cast::(); 123 | std::mem::forget(self); 124 | ptr 125 | } 126 | } 127 | 128 | #[cfg(any(test, feature = "proptest"))] 129 | impl Arbitrary for LibcString { 130 | type Parameters = (); 131 | type Strategy = prop::strategy::Perturb< 132 | crate::strategies::AnyString, 133 | fn(String, prop::test_runner::TestRng) -> Self, 134 | >; 135 | 136 | fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { 137 | crate::strategies::any_string().prop_perturb(|s, mut rng| { 138 | let s = s 139 | .chars() 140 | .map(|c| { 141 | if c == '\0' { 142 | char::from(rng.random_range(1..=127)) 143 | } else { 144 | c 145 | } 146 | }) 147 | .collect::(); 148 | Self::new(s).expect("input was sanitized above, can't fail") 149 | }) 150 | } 151 | } 152 | 153 | impl AsRef for LibcString { 154 | fn as_ref(&self) -> &str { 155 | self.as_str() 156 | } 157 | } 158 | 159 | impl Clone for LibcString { 160 | fn clone(&self) -> Self { 161 | // Allocate C string and wrap it in Self for auto-deallocation 162 | let len = self.len(); 163 | // SAFETY: malloc is safe to call for any nonzero length 164 | let buf = unsafe { libc::malloc(len) }.cast::(); 165 | let buf = NonNull::new(buf).expect("Failed to allocate string buffer"); 166 | // Eventually using this slice pointer is safe because... 167 | // - buf is valid for reads and writes for len bytes 168 | // - Byte pointers are always aligned 169 | // - buf will be initialized and unaliased by the time this pointer is 170 | // dereferenced 171 | // - len was checked to fit Rust's slice size requirements 172 | let buf = NonNull::slice_from_raw_parts(buf, len); 173 | let result = Self(buf); 174 | 175 | // Fill the string and return it 176 | let output_start = buf.as_ptr().cast::(); 177 | let input_start = self.as_ref().as_bytes().as_ptr(); 178 | // SAFETY: - By definition of a slice, the source range is correct 179 | // - It is OK to read len bytes as input has that many bytes 180 | // - It is OK to write len bytes as output has that many bytes 181 | // - Byte pointers are always aligned 182 | // - Overlap between unrelated memory allocations is impossible 183 | unsafe { output_start.copy_from_nonoverlapping(input_start, len) }; 184 | result 185 | } 186 | } 187 | 188 | impl Debug for LibcString { 189 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 190 | let s: &str = self.as_ref(); 191 | f.pad(&format!("{s:?}")) 192 | } 193 | } 194 | 195 | impl Display for LibcString { 196 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 197 | let s: &str = self.as_ref(); 198 | f.pad(s) 199 | } 200 | } 201 | 202 | impl Drop for LibcString { 203 | fn drop(&mut self) { 204 | // SAFETY: self.0 comes from malloc and there is no way to deallocate it 205 | // other than Drop so double free cannot happen 206 | unsafe { libc::free(self.0.as_ptr().cast::()) } 207 | } 208 | } 209 | 210 | impl Eq for LibcString {} 211 | 212 | impl PartialEq for LibcString { 213 | fn eq(&self, other: &Self) -> bool { 214 | self.as_ref() == other.as_ref() 215 | } 216 | } 217 | 218 | // SAFETY: LibcString exposes no internal mutability 219 | unsafe impl Send for LibcString {} 220 | 221 | // SAFETY: LibcString exposes no internal mutability 222 | unsafe impl Sync for LibcString {} 223 | 224 | #[cfg(test)] 225 | mod tests { 226 | use super::*; 227 | use crate::strategies::any_string; 228 | #[allow(unused)] 229 | use similar_asserts::assert_eq; 230 | use std::ffi::CStr; 231 | 232 | macro_rules! check_new { 233 | ($result:expr, $input:expr) => { 234 | match ($input.contains('\0'), $result) { 235 | (false, Ok(result)) => result, 236 | (true, Err(NulError)) => return Ok(()), 237 | (false, Err(NulError)) | (true, Ok(_)) => { 238 | prop_assert!( 239 | false, 240 | "LibcString::new should error out if and only if NUL is present" 241 | ); 242 | unreachable!() 243 | } 244 | } 245 | }; 246 | } 247 | 248 | proptest! { 249 | #[test] 250 | fn unary(s in any_string()) { 251 | // Set up a C string 252 | let c = check_new!(LibcString::new(&s), &s); 253 | 254 | // Test basic properties 255 | prop_assert_eq!(c.len(), s.len() + 1); 256 | // SAFETY: Safe as a type invariant of LibcString 257 | prop_assert_eq!(unsafe { CStr::from_ptr(c.borrow()) }.to_str().unwrap(), &s); 258 | prop_assert_eq!(c.as_ref(), &s); 259 | prop_assert_eq!(&c.to_string(), &s); 260 | prop_assert_eq!(format!("{c:?}"), format!("{s:?}")); 261 | 262 | // Test cloning 263 | let c2 = c.clone(); 264 | prop_assert_eq!(c2.len(), c.len()); 265 | prop_assert_ne!(c2.borrow(), c.borrow()); 266 | prop_assert_eq!(c2.as_ref(), c.as_ref()); 267 | 268 | // Test raw char* extraction 269 | let backup = c.0; 270 | // SAFETY: Will not actually be sent to a C API 271 | let raw = unsafe { c.into_raw() }; 272 | prop_assert_eq!(raw, backup.cast::().as_ptr()); 273 | 274 | // SAFETY: Effectively replicates Drop. Correct because since into_raw() 275 | // has been called, Drop will not be called. 276 | unsafe { libc::free(raw.cast::()) }; 277 | } 278 | 279 | #[test] 280 | fn binary((s1, s2) in (any_string(), any_string())) { 281 | let c1 = check_new!(LibcString::new(&s1), &s1); 282 | let c2 = check_new!(LibcString::new(&s2), &s2); 283 | prop_assert_eq!(c1 == c2, s1 == s2); 284 | } 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /src/ffi/transparent.rs: -------------------------------------------------------------------------------- 1 | //! `repr(transparent)` newtypes over hwloc types 2 | //! 3 | //! A safe Rust binding to a C library can only function as intended if 4 | //! structs of interest are kept in a valid state. For example pointers should 5 | //! keep pointing to valid allocations containing valid data of the expected 6 | //! lifetimes, be devoid of forbidden aliases when dereferenced, etc. 7 | //! In other words, for safe Rust -> C bindings to be possible, C struct type 8 | //! invariants must be enforced in such a way that safe code cannot modify the 9 | //! struct in a manner that violates them. 10 | //! 11 | //! However, given access to a C struct's field access can trivially break its 12 | //! invariants (e.g. replace a pointer that shouldn't be null with a null one, 13 | //! replace a pointer to something of lifetime 'a with a pointer to another 14 | //! thing of lifetime 'b). Therefore, type invariant preservation requires 15 | //! field encapsulation. 16 | //! 17 | //! At the same time, we want to... 18 | //! 19 | //! - Have the raw `hwlocality-sys` FFI layer be purely defined in terms of 20 | //! un-encapsulated C struct definitions. 21 | //! - Be able to translate a pointer to an un-encapsulated struct from the FFI 22 | //! layer into a pointer/reference to an encapsulated struct without needing 23 | //! any complicated/expensive code. 24 | //! 25 | //! The way to fulfill these constraints in Rust is to use 26 | //! `repr(transparent)` newtypes for encapsulation, and this module is all about 27 | //! doing that while minimizing the safety impact of all the pointer casting 28 | //! that such a design inevitably leads to. 29 | 30 | #[allow(unused)] 31 | #[cfg(test)] 32 | use similar_asserts::assert_eq; 33 | use std::ptr::NonNull; 34 | 35 | /// Type that is a `repr(transparent)` wrapper around another type 36 | /// 37 | /// # Safety 38 | /// 39 | /// `Self` must be a `repr(transparent)` wrapper around `Self::Inner` 40 | pub(crate) unsafe trait TransparentNewtype: Sized { 41 | /// Inner type that is being wrapped by `Self` 42 | type Inner; 43 | 44 | /// Assert that `Self` has the same size and alignment as `Self::Inner` 45 | /// 46 | /// This minimal layout check should be passed by any valid implementation 47 | /// of this trait, and be fully evaluated at compile time in release builds 48 | /// with no associated runtime costs. 49 | #[inline] 50 | fn check_basic_layout() { 51 | use std::mem::{align_of, size_of}; 52 | /// Outlined to mitigate generics code bloat 53 | #[cold] 54 | fn failed() -> ! { 55 | panic!("Invalid TransparentNewtype impl detected!"); 56 | } 57 | if size_of::() != size_of::() { 58 | failed() 59 | } 60 | if align_of::() != align_of::() { 61 | failed() 62 | } 63 | } 64 | } 65 | 66 | /// Convert a pointer/reference to a C-style struct into the newtype equivalent 67 | /// 68 | /// # Safety 69 | /// 70 | /// Unsafe code can rely on this trait being implemented correctly for safety 71 | #[allow(clippy::wrong_self_convention)] 72 | pub(crate) unsafe trait AsNewtype { 73 | /// Like this type, but with the C struct replaced with its newtype 74 | type Wrapped; 75 | 76 | /// Perform the conversion 77 | /// 78 | /// # Safety 79 | /// 80 | /// The C-style struct must match the expectations of the higher-level 81 | /// newtype wrapper, which typically comes with extra safety invariants. 82 | unsafe fn as_newtype(self) -> Self::Wrapped; 83 | } 84 | 85 | // SAFETY: Per TransparentNewtype contract, casting &'a T to &'a NewT is legal 86 | unsafe impl<'a, T, NewT: TransparentNewtype + 'a> AsNewtype for &'a T { 87 | type Wrapped = &'a NewT; 88 | 89 | unsafe fn as_newtype(self) -> Self::Wrapped { 90 | NewT::check_basic_layout(); 91 | let ptr: *const T = self; 92 | // SAFETY: - &mut *ptr is safe per se since ptr is just a reinterpreted &mut 93 | // - Per TransparentNewtype contract, T can be reinterpreted as NewT 94 | unsafe { &*ptr.cast::() } 95 | } 96 | } 97 | 98 | // SAFETY: Per TransparentNewtype contract, casting &'a mut T to &'a mut NewT is legal 99 | unsafe impl<'a, T, NewT: TransparentNewtype + 'a> AsNewtype for &'a mut T { 100 | type Wrapped = &'a mut NewT; 101 | 102 | unsafe fn as_newtype(self) -> Self::Wrapped { 103 | NewT::check_basic_layout(); 104 | let ptr: *mut T = self; 105 | // SAFETY: - &mut *ptr is safe per se since ptr is just a reinterpreted &mut 106 | // - Per TransparentNewtype contract, T can be reinterpreted as NewT 107 | unsafe { &mut *ptr.cast::() } 108 | } 109 | } 110 | 111 | // SAFETY: Per TransparentNewtype contract, casting NonNull to NonNull is legal 112 | unsafe impl> AsNewtype for NonNull { 113 | type Wrapped = NonNull; 114 | 115 | unsafe fn as_newtype(self) -> Self::Wrapped { 116 | NewT::check_basic_layout(); 117 | self.cast() 118 | } 119 | } 120 | 121 | // SAFETY: Per TransparentNewtype contract, casting *const T to *const NewT is legal 122 | unsafe impl> AsNewtype for *const T { 123 | type Wrapped = *const NewT; 124 | 125 | unsafe fn as_newtype(self) -> Self::Wrapped { 126 | NewT::check_basic_layout(); 127 | self.cast() 128 | } 129 | } 130 | 131 | // SAFETY: Per TransparentNewtype contract, casting *mut T to *mut NewT is legal 132 | unsafe impl> AsNewtype for *mut T { 133 | type Wrapped = *mut NewT; 134 | 135 | unsafe fn as_newtype(self) -> Self::Wrapped { 136 | NewT::check_basic_layout(); 137 | self.cast() 138 | } 139 | } 140 | 141 | /// Convert a pointer/reference to a newtype into the inner struct equivalent 142 | /// 143 | /// # Safety 144 | /// 145 | /// Unsafe code can rely on this trait being implemented correctly for safety 146 | #[allow(clippy::wrong_self_convention)] 147 | #[allow(unused)] 148 | pub(crate) unsafe trait AsInner { 149 | /// Like this type, but with the newtype replaced with its inner struct 150 | type Unwrapped; 151 | 152 | /// Perform the conversion 153 | fn as_inner(self) -> Self::Unwrapped; 154 | } 155 | 156 | // SAFETY: Per TransparentNewtype contract, casting &'a NewT to &'a NewT::Inner is legal 157 | unsafe impl<'a, NewT: TransparentNewtype + 'a> AsInner for &'a NewT { 158 | type Unwrapped = &'a NewT::Inner; 159 | 160 | fn as_inner(self) -> Self::Unwrapped { 161 | NewT::check_basic_layout(); 162 | let ptr: *const NewT = self; 163 | // SAFETY: - &mut *ptr is safe per se since ptr is just a reinterpreted &mut 164 | // - Per TransparentNewtype contract, NewT can be reinterpreted 165 | // as its inner type 166 | unsafe { &*ptr.cast::() } 167 | } 168 | } 169 | 170 | // SAFETY: Per TransparentNewtype contract, casting &'a mut NewT to &'a mut NewT::Inner is legal 171 | unsafe impl<'a, NewT: TransparentNewtype + 'a> AsInner for &'a mut NewT { 172 | type Unwrapped = &'a mut NewT::Inner; 173 | 174 | fn as_inner(self) -> Self::Unwrapped { 175 | NewT::check_basic_layout(); 176 | let ptr: *mut NewT = self; 177 | // SAFETY: - &mut *ptr is safe per se since ptr is just a reinterpreted &mut 178 | // - Per TransparentNewtype contract, NewT can be reinterpreted 179 | // as its inner type 180 | unsafe { &mut *ptr.cast::() } 181 | } 182 | } 183 | 184 | // SAFETY: Per TransparentNewtype contract, casting NonNull to NonNull is legal 185 | unsafe impl AsInner for NonNull { 186 | type Unwrapped = NonNull; 187 | 188 | fn as_inner(self) -> Self::Unwrapped { 189 | NewT::check_basic_layout(); 190 | self.cast() 191 | } 192 | } 193 | 194 | // SAFETY: Per TransparentNewtype contract, casting *const NewT to *const NewT::Inner is legal 195 | unsafe impl AsInner for *const NewT { 196 | type Unwrapped = *const NewT::Inner; 197 | 198 | fn as_inner(self) -> Self::Unwrapped { 199 | NewT::check_basic_layout(); 200 | self.cast() 201 | } 202 | } 203 | 204 | // SAFETY: Per TransparentNewtype contract, casting *mut NewT to *mut NewT::Inner is legal 205 | unsafe impl AsInner for *mut NewT { 206 | type Unwrapped = *mut NewT::Inner; 207 | 208 | fn as_inner(self) -> Self::Unwrapped { 209 | NewT::check_basic_layout(); 210 | self.cast() 211 | } 212 | } 213 | 214 | #[cfg(test)] 215 | mod tests { 216 | use super::*; 217 | use crate::info::TextualInfo; 218 | use hwlocality_sys::hwloc_info_s; 219 | #[allow(unused)] 220 | use similar_asserts::assert_eq; 221 | use std::ptr; 222 | 223 | #[test] 224 | fn transparent_newtype() { 225 | let mut info = hwloc_info_s { 226 | name: ptr::null_mut(), 227 | value: ptr::null_mut(), 228 | }; 229 | let nonnull_info = NonNull::from(&mut info); 230 | let mut_info = nonnull_info.as_ptr(); 231 | let const_info: *const hwloc_info_s = mut_info; 232 | TextualInfo::check_basic_layout(); 233 | { 234 | // SAFETY: info is in a known-good state 235 | let r = unsafe { <&hwloc_info_s as AsNewtype>::as_newtype(&info) }; 236 | let p: *const TextualInfo = r; 237 | assert_eq!(p.cast::(), const_info); 238 | assert!(ptr::eq(<&TextualInfo as AsInner>::as_inner(r), const_info)); 239 | } 240 | { 241 | // SAFETY: info is in a known-good state 242 | let r = unsafe { <&mut hwloc_info_s as AsNewtype>::as_newtype(&mut info) }; 243 | let p: *mut TextualInfo = r; 244 | assert_eq!(p.cast::(), mut_info); 245 | assert!(ptr::eq( 246 | <&mut TextualInfo as AsInner>::as_inner(r), 247 | mut_info 248 | )); 249 | } 250 | { 251 | // SAFETY: info is in a known-good state 252 | let p: NonNull = unsafe { 253 | as AsNewtype>::as_newtype(nonnull_info) 254 | }; 255 | assert_eq!(p.cast::(), nonnull_info); 256 | assert_eq!( as AsInner>::as_inner(p), nonnull_info); 257 | } 258 | { 259 | let p: *const TextualInfo = 260 | // SAFETY: info is in a known-good state 261 | unsafe { <*const hwloc_info_s as AsNewtype>::as_newtype(const_info) }; 262 | assert_eq!(p.cast::(), const_info); 263 | assert_eq!(<*const TextualInfo as AsInner>::as_inner(p), const_info); 264 | } 265 | { 266 | let p: *mut TextualInfo = 267 | // SAFETY: info is in a known-good state 268 | unsafe { <*mut hwloc_info_s as AsNewtype>::as_newtype(mut_info) }; 269 | assert_eq!(p.cast::(), mut_info); 270 | assert_eq!(<*mut TextualInfo as AsInner>::as_inner(p), mut_info); 271 | } 272 | } 273 | 274 | // SAFETY: This is actually unsafe, don't do this! 275 | // 276 | // The only purpose of this impl is to test the last-resort safety nets. 277 | unsafe impl TransparentNewtype for () { 278 | type Inner = u8; 279 | } 280 | // 281 | // SAFETY: This is actually unsafe, don't do this! 282 | // 283 | // The only purpose of this impl is to test the last-resort safety nets. 284 | unsafe impl TransparentNewtype for u16 { 285 | type Inner = [u8; 2]; 286 | } 287 | 288 | #[test] 289 | #[should_panic] 290 | fn bad_transparent_newtype_size() { 291 | <()>::check_basic_layout(); 292 | } 293 | 294 | #[test] 295 | #[should_panic] 296 | fn bad_transparent_newtype_alignment() { 297 | u16::check_basic_layout(); 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /src/ffi/unknown.rs: -------------------------------------------------------------------------------- 1 | //! Handling of unknown enum variants 2 | 3 | use derive_more::{AsRef, Binary, Deref, Display, LowerExp, LowerHex, Octal, UpperExp, UpperHex}; 4 | 5 | /// Unknown enum variant from hwloc 6 | /// 7 | /// Values of this type represent enum values received from hwloc that 8 | /// `hwlocality` was not built to handle. This can happen when `hwlocality` is 9 | /// built to support a certain minimal hwloc version, but is then linked 10 | /// against a newer hwloc version that defines new enum variants. 11 | /// 12 | /// You may freely introspect the contents of these enum values, but are not 13 | /// allowed to generate new ones from Rust integers. This lets `hwlocality` 14 | /// assume that these values are valid inputs that your hwloc version can accept 15 | /// (since it originally emitted them as the output of another query). And 16 | /// therefore you will be able to feed these values back to hwloc in most 17 | /// circumstances. 18 | /// 19 | /// This capability to feed back unknown enum values to hwloc may however be 20 | /// restricted in circumstances where hwloc treats enum variants in such a way 21 | /// that sending it an unknown enum variant could result in a memory-, type- or 22 | /// thread-safety hazard. 23 | #[derive( 24 | AsRef, 25 | Binary, 26 | Copy, 27 | Clone, 28 | Debug, 29 | Deref, 30 | Display, 31 | Eq, 32 | Hash, 33 | LowerExp, 34 | LowerHex, 35 | Octal, 36 | Ord, 37 | PartialEq, 38 | PartialOrd, 39 | UpperExp, 40 | UpperHex, 41 | )] 42 | pub struct UnknownVariant(pub(crate) T); 43 | // 44 | impl UnknownVariant { 45 | /// Access the inner hwloc enum value 46 | pub fn get(self) -> T { 47 | self.0 48 | } 49 | } 50 | 51 | #[cfg(test)] 52 | mod tests { 53 | use super::*; 54 | use proptest::prelude::*; 55 | #[allow(unused)] 56 | use similar_asserts::assert_eq; 57 | 58 | proptest! { 59 | #[test] 60 | fn unknown_get(v: u64) { 61 | prop_assert_eq!(UnknownVariant(v).get(), v); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/info.rs: -------------------------------------------------------------------------------- 1 | //! Textual key-value information 2 | //! 3 | //! Several hwloc entities may be freely annotated with free-form textual 4 | //! information in a key-value layout. This module provides an interface to 5 | //! this information. 6 | 7 | use crate::ffi::{self, string::LibcString, transparent::TransparentNewtype}; 8 | use hwlocality_sys::hwloc_info_s; 9 | #[allow(unused)] 10 | #[cfg(test)] 11 | use similar_asserts::assert_eq; 12 | use std::{ffi::CStr, fmt, hash::Hash}; 13 | 14 | /// Textual key-value information 15 | /// 16 | /// Used in multiple places of the hwloc API to provide extensible free-form 17 | /// textual metadata. 18 | /// 19 | /// You cannot create an owned object of this type, it belongs to the topology. 20 | // 21 | // --- Implementation details --- 22 | // 23 | // # Safety 24 | // 25 | // As a type invariant, all inner pointers are assumed to be safe to 26 | // dereference, and pointing to a valid C string devoid of mutable aliases, as 27 | // long as the TextualInfo is reachable at all. 28 | // 29 | // This is enforced through the following precautions: 30 | // 31 | // - No public API exposes an owned TextualInfo, only references to it bound by 32 | // the parent topology's lifetime are exposed 33 | // - APIs for interacting with topologies and textual info honor Rust's 34 | // shared XOR mutable aliasing rules, with no internal mutability 35 | // - new() explicitly warns about associated aliasing/validity dangers 36 | // 37 | // Provided that objects do not link to strings allocated outside of the 38 | // topology they originate from, which is a minimally sane expectation from 39 | // hwloc, this should be enough. 40 | #[allow(clippy::non_send_fields_in_send_ty, missing_copy_implementations)] 41 | #[doc(alias = "hwloc_info_s")] 42 | #[repr(transparent)] 43 | pub struct TextualInfo(hwloc_info_s); 44 | 45 | impl TextualInfo { 46 | /// Build a `hwloc_info_s` struct for hwloc consumption 47 | /// 48 | /// # Safety 49 | /// 50 | /// - Do not modify the target [`LibcString`]s as long as this is used 51 | /// - Do not use after the associated [`LibcString`]s have been dropped 52 | #[allow(unused)] 53 | pub(crate) unsafe fn borrow_raw(name: &LibcString, value: &LibcString) -> hwloc_info_s { 54 | hwloc_info_s { 55 | name: name.borrow().cast_mut(), 56 | value: value.borrow().cast_mut(), 57 | } 58 | } 59 | 60 | /// Name indicating which information is being provided 61 | #[doc(alias = "hwloc_info_s::name")] 62 | pub fn name(&self) -> &CStr { 63 | // SAFETY: - Pointer validity is assumed as a type invariant 64 | // - Rust aliasing rules are enforced by deriving the reference 65 | // from &self, which itself is derived from &Topology 66 | unsafe { ffi::deref_str(&self.0.name) }.expect("Infos should have names") 67 | } 68 | 69 | /// Information in textual form 70 | #[doc(alias = "hwloc_info_s::value")] 71 | pub fn value(&self) -> &CStr { 72 | // SAFETY: - Pointer validity is assumed as a type invariant 73 | // - Rust aliasing rules are enforced by deriving the reference 74 | // from &self, which itself is derived from &Topology 75 | unsafe { ffi::deref_str(&self.0.value) }.expect("Infos should have values") 76 | } 77 | } 78 | 79 | impl fmt::Debug for TextualInfo { 80 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 81 | f.debug_struct("TextualInfo") 82 | .field("name", &self.name()) 83 | .field("value", &self.value()) 84 | .finish() 85 | } 86 | } 87 | 88 | impl Eq for TextualInfo {} 89 | 90 | impl Hash for TextualInfo { 91 | fn hash(&self, state: &mut H) { 92 | self.name().hash(state); 93 | self.value().hash(state); 94 | } 95 | } 96 | 97 | impl PartialEq for TextualInfo { 98 | fn eq(&self, other: &Self) -> bool { 99 | self.name() == other.name() && self.value() == other.value() 100 | } 101 | } 102 | 103 | // SAFETY: Does not have internal mutability 104 | unsafe impl Send for TextualInfo {} 105 | 106 | // SAFETY: Does not have internal mutability 107 | unsafe impl Sync for TextualInfo {} 108 | 109 | // SAFETY: TextualInfo is a repr(transparent) newtype of hwloc_info_s 110 | unsafe impl TransparentNewtype for TextualInfo { 111 | type Inner = hwloc_info_s; 112 | } 113 | 114 | #[cfg(test)] 115 | mod tests { 116 | use super::*; 117 | use crate::ffi::transparent::AsNewtype; 118 | use proptest::prelude::*; 119 | #[allow(unused)] 120 | use similar_asserts::assert_eq; 121 | use static_assertions::{assert_impl_all, assert_not_impl_any}; 122 | use std::{ 123 | collections::hash_map::RandomState, 124 | ffi::CString, 125 | fmt::{Binary, Debug, Display, LowerExp, LowerHex, Octal, Pointer, UpperExp, UpperHex}, 126 | hash::{BuildHasher, Hasher}, 127 | io::{self, Read}, 128 | ops::Deref, 129 | panic::UnwindSafe, 130 | }; 131 | 132 | // Check that public types in this module keep implementing all expected 133 | // traits, in the interest of detecting future semver-breaking changes 134 | assert_impl_all!(TextualInfo: 135 | Debug, Hash, Sized, Sync, Unpin, UnwindSafe 136 | ); 137 | assert_not_impl_any!(TextualInfo: 138 | Binary, Clone, Default, Deref, Display, Drop, IntoIterator, 139 | LowerExp, LowerHex, Octal, PartialOrd, Pointer, Read, ToOwned, 140 | UpperExp, UpperHex, fmt::Write, io::Write 141 | ); 142 | 143 | proptest! { 144 | #[test] 145 | fn unary(name: LibcString, value: LibcString) { 146 | // Set up test entity 147 | // SAFETY: `name` and `value` won't be invalidated while this exists 148 | let raw_info = unsafe { TextualInfo::borrow_raw(&name, &value) }; 149 | // SAFETY: raw_info was built from known-good data 150 | let info: &TextualInfo = unsafe { (&raw_info).as_newtype() }; 151 | 152 | // Check raw data 153 | prop_assert_eq!(info.0.name, name.borrow().cast_mut()); 154 | prop_assert_eq!(info.0.value, value.borrow().cast_mut()); 155 | 156 | // Check high-level accessors 157 | let name_c = CString::new(name.as_ref()).unwrap(); 158 | let value_c = CString::new(value.as_ref()).unwrap(); 159 | prop_assert_eq!(&CString::from(info.name()), &name_c); 160 | prop_assert_eq!(&CString::from(info.value()), &value_c); 161 | prop_assert_eq!( 162 | format!("{info:#?}"), 163 | format!( 164 | "TextualInfo {{\n \ 165 | name: {name_c:?},\n \ 166 | value: {value_c:?},\n\ 167 | }}", 168 | ) 169 | ); 170 | 171 | // Check hashing 172 | let state = RandomState::new(); 173 | let mut expected_hasher = state.build_hasher(); 174 | name_c.hash(&mut expected_hasher); 175 | value_c.hash(&mut expected_hasher); 176 | let expected_hash = expected_hasher.finish(); 177 | let actual_hash = state.hash_one(info); 178 | prop_assert_eq!(actual_hash, expected_hash); 179 | } 180 | 181 | #[test] 182 | fn binary(name1: LibcString, name2: LibcString, value1: LibcString, value2: LibcString) { 183 | // Set up test entity 184 | // SAFETY: `name` and `value` won't be invalidated while this exists 185 | let raw_info1 = unsafe { TextualInfo::borrow_raw(&name1, &value1) }; 186 | // SAFETY: raw_info1 was built from known-good data 187 | let info1: &TextualInfo = unsafe { (&raw_info1).as_newtype() }; 188 | // SAFETY: `name` and `value` won't be invalidated while this exists 189 | let raw_info2 = unsafe { TextualInfo::borrow_raw(&name2, &value2) }; 190 | // SAFETY: raw_info2 was built from known-good data 191 | let info2: &TextualInfo = unsafe { (&raw_info2).as_newtype() }; 192 | 193 | // Check equality 194 | prop_assert_eq!(info1 == info2, name1 == name2 && value1 == value2); 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/interop/linux.rs: -------------------------------------------------------------------------------- 1 | //! Linux-specific helpers 2 | 3 | #[cfg(doc)] 4 | use crate::cpu::binding::CpuBindingFlags; 5 | use crate::{ 6 | cpu::cpuset::CpuSet, 7 | errors::{self, HybridError, RawHwlocError}, 8 | path::{self, PathError}, 9 | topology::Topology, 10 | }; 11 | #[allow(unused)] 12 | #[cfg(test)] 13 | use similar_asserts::assert_eq; 14 | use std::{ops::Deref, path::Path}; 15 | 16 | // This file is rustdoc-visible so we must provide a substitute for 17 | // linux-specific libc entities when people run rustdoc on Windows. 18 | #[cfg(target_os = "linux")] 19 | use libc::pid_t; 20 | #[cfg(all(doc, not(target_os = "linux")))] 21 | #[allow(non_camel_case_types)] 22 | struct pid_t; 23 | 24 | /// # Linux-specific helpers 25 | /// 26 | /// This includes helpers for manipulating Linux kernel cpumap files, and hwloc 27 | /// equivalents of the Linux `sched_setaffinity` and `sched_getaffinity` system 28 | /// calls. 29 | // 30 | // --- Implementation details --- 31 | // 32 | // Upstream docs: https://hwloc.readthedocs.io/en/stable/group__hwlocality__linux.html 33 | impl Topology { 34 | /// Bind a thread `tid` on cpus given in `set` 35 | /// 36 | /// `set` can be a `&'_ CpuSet` or a `BitmapRef<'_, CpuSet>`. 37 | /// 38 | /// The behavior is exactly the same as the Linux `sched_setaffinity` system 39 | /// call, but uses a hwloc [`CpuSet`]. 40 | /// 41 | /// This is equivalent to calling [`bind_process_cpu()`] with the [`THREAD`] 42 | /// binding flag. 43 | /// 44 | /// [`bind_process_cpu()`]: Topology::bind_process_cpu() 45 | /// [`THREAD`]: CpuBindingFlags::THREAD 46 | #[allow(clippy::missing_errors_doc)] 47 | #[doc(alias = "hwloc_linux_set_tid_cpubind")] 48 | pub fn bind_tid_cpu( 49 | &self, 50 | tid: pid_t, 51 | set: impl Deref, 52 | ) -> Result<(), RawHwlocError> { 53 | /// Polymorphized version of this function (avoids generics code bloat) 54 | fn polymorphized(self_: &Topology, tid: pid_t, set: &CpuSet) -> Result<(), RawHwlocError> { 55 | // SAFETY: - Topology is trusted to contain a valid ptr (type invariant) 56 | // - Bitmap is trusted to contain a valid ptr (type invariant) 57 | // - hwloc ops are trusted not to modify *const parameters 58 | // - TID cannot be validated (think TOCTOU), but hwloc should be 59 | // able to handle an invalid TID 60 | errors::call_hwloc_zero_or_minus1("hwloc_linux_set_tid_cpubind", || unsafe { 61 | hwlocality_sys::hwloc_linux_set_tid_cpubind(self_.as_ptr(), tid, set.as_ptr()) 62 | }) 63 | } 64 | polymorphized(self, tid, &set) 65 | } 66 | 67 | /// Current binding of thread `tid`. 68 | /// 69 | /// Returns the [`CpuSet`] of PUs which the thread was last bound to. 70 | /// 71 | /// The behavior is exactly the same as the Linux `sched_getaffinity` system 72 | /// call, but uses a hwloc [`CpuSet`]. 73 | /// 74 | /// This is equivalent to calling [`process_cpu_binding()`] with the 75 | /// [`THREAD`] binding flag. 76 | /// 77 | /// [`process_cpu_binding()`]: Topology::process_cpu_binding() 78 | /// [`THREAD`]: CpuBindingFlags::THREAD 79 | #[allow(clippy::missing_errors_doc)] 80 | #[doc(alias = "hwloc_linux_get_tid_cpubind")] 81 | pub fn tid_cpu_binding(&self, tid: pid_t) -> Result { 82 | let mut set = CpuSet::new(); 83 | // SAFETY: - Topology is trusted to contain a valid ptr (type invariant) 84 | // - Bitmap is trusted to contain a valid ptr (type invariant) 85 | // - hwloc ops are trusted not to modify *const parameters 86 | // - hwloc ops are trusted to keep *mut parameters in a 87 | // valid state unless stated otherwise 88 | // - TID cannot be validated (think TOCTOU), but hwloc should be 89 | // able to handle an invalid TID 90 | errors::call_hwloc_zero_or_minus1("hwloc_linux_get_tid_cpubind", || unsafe { 91 | hwlocality_sys::hwloc_linux_get_tid_cpubind(self.as_ptr(), tid, set.as_mut_ptr()) 92 | }) 93 | .map(|()| set) 94 | } 95 | 96 | /// Last physical CPU where thread `tid` ran. 97 | /// 98 | /// Indicates the PU which the thread last ran on, as a singleton [`CpuSet`]. 99 | /// 100 | /// This is equivalent to calling [`last_process_cpu_location()`] with the 101 | /// [`THREAD`] binding flag. 102 | /// 103 | /// [`last_process_cpu_location()`]: Topology::last_process_cpu_location() 104 | /// [`THREAD`]: CpuBindingFlags::THREAD 105 | #[allow(clippy::missing_errors_doc)] 106 | #[doc(alias = "hwloc_linux_get_tid_last_cpu_location")] 107 | pub fn last_tid_cpu_location(&self, tid: pid_t) -> Result { 108 | let mut set = CpuSet::new(); 109 | // SAFETY: - Topology is trusted to contain a valid ptr (type invariant) 110 | // - Bitmap is trusted to contain a valid ptr (type invariant) 111 | // - hwloc ops are trusted not to modify *const parameters 112 | // - hwloc ops are trusted to keep *mut parameters in a 113 | // valid state unless stated otherwise 114 | // - TID cannot be validated (think TOCTOU), but hwloc should be 115 | // able to handle an invalid TID 116 | errors::call_hwloc_zero_or_minus1("hwloc_linux_get_tid_last_cpu_location", || unsafe { 117 | hwlocality_sys::hwloc_linux_get_tid_last_cpu_location( 118 | self.as_ptr(), 119 | tid, 120 | set.as_mut_ptr(), 121 | ) 122 | }) 123 | .map(|()| set) 124 | } 125 | 126 | /// Convert a linux kernel cpumask file path into a hwloc bitmap set. 127 | /// 128 | /// Might be used when reading CPU sets from sysfs attributes such as 129 | /// `topology` and `caches` for processors, or `local_cpus` for devices. 130 | /// 131 | /// Note that this function ignores the [HWLOC_FSROOT environment 132 | /// variable](https://hwloc.readthedocs.io/en/stable/envvar.html). 133 | /// 134 | /// # Errors 135 | /// 136 | /// - [`ContainsNul`] if `path` contains NUL chars. 137 | /// - [`NotUnicode`] if `path` contains non-Unicode data 138 | /// 139 | /// # Example 140 | /// 141 | #[cfg_attr(target_os = "linux", doc = "```rust")] 142 | #[cfg_attr(not(target_os = "linux"), doc = "```rust,ignore")] 143 | /// # use hwlocality::{topology::Topology, object::types::ObjectType}; 144 | /// # 145 | /// # let topology = Topology::test_instance(); 146 | /// # 147 | /// use eyre::eyre; 148 | /// 149 | /// // Read cpuset of first core via Linux sysfs 150 | /// let core_set = topology 151 | /// .read_path_as_cpumask("/sys/devices/system/cpu/cpu0/topology/core_cpus")?; 152 | /// 153 | /// // Check that the hwloc version is consistent 154 | /// assert_eq!( 155 | /// core_set, 156 | /// topology 157 | /// .objects_with_type(ObjectType::Core) 158 | /// .next() 159 | /// .ok_or_else(|| eyre!("Linux system should have CPU cores"))? 160 | /// .cpuset() 161 | /// .ok_or_else(|| eyre!("CPU cores should have cpusets"))? 162 | /// ); 163 | /// # 164 | /// # Ok::<(), eyre::Report>(()) 165 | /// ``` 166 | /// 167 | /// [`ContainsNul`]: PathError::ContainsNul 168 | /// [`NotUnicode`]: PathError::NotUnicode 169 | #[doc(alias = "hwloc_linux_read_path_as_cpumask")] 170 | pub fn read_path_as_cpumask( 171 | &self, 172 | path: impl AsRef, 173 | ) -> Result> { 174 | /// Polymorphized version of this function (avoids generics code bloat) 175 | fn polymorphized(path: &Path) -> Result> { 176 | let path = path::make_hwloc_path(path)?; 177 | let mut set = CpuSet::new(); 178 | // SAFETY: - Path is trusted to contain a valid C string (type invariant) 179 | // - Bitmap is trusted to contain a valid ptr (type invariant) 180 | // - hwloc ops are trusted not to modify *const parameters 181 | // - hwloc ops are trusted to keep *mut parameters in a 182 | // valid state unless stated otherwise 183 | errors::call_hwloc_zero_or_minus1("hwloc_linux_read_path_as_cpumask", || unsafe { 184 | hwlocality_sys::hwloc_linux_read_path_as_cpumask(path.borrow(), set.as_mut_ptr()) 185 | }) 186 | .map(|()| set) 187 | .map_err(HybridError::Hwloc) 188 | } 189 | polymorphized(path.as_ref()) 190 | } 191 | } 192 | 193 | #[cfg(test)] 194 | mod tests { 195 | use super::*; 196 | use crate::{ 197 | cpu::binding::CpuBindingFlags, object::types::ObjectType, strategies::topology_related_set, 198 | }; 199 | use proptest::prelude::*; 200 | #[allow(unused)] 201 | use similar_asserts::assert_eq; 202 | 203 | #[test] 204 | fn read_path_as_cpumask() { 205 | let topology = Topology::test_instance(); 206 | 207 | // Read cpuset of first core via Linux sysfs 208 | let core_set = topology 209 | .read_path_as_cpumask("/sys/devices/system/cpu/cpu0/topology/core_cpus") 210 | .unwrap(); 211 | 212 | // Compare with hwloc result 213 | assert_eq!( 214 | core_set, 215 | topology 216 | .objects_with_type(ObjectType::Core) 217 | .next() 218 | .unwrap() 219 | .cpuset() 220 | .unwrap() 221 | ); 222 | } 223 | 224 | #[test] 225 | fn initial_tid_cpubind() { 226 | // SAFETY: Should always be safe to call 227 | let my_tid = unsafe { libc::gettid() }; 228 | let topology = Topology::test_instance(); 229 | 230 | let my_cpu_binding = topology 231 | .process_cpu_binding(my_tid.try_into().unwrap(), CpuBindingFlags::THREAD) 232 | .unwrap(); 233 | assert_eq!(topology.tid_cpu_binding(my_tid).unwrap(), my_cpu_binding); 234 | 235 | let last_cpu_location = topology.last_tid_cpu_location(my_tid).unwrap(); 236 | assert_eq!(last_cpu_location.weight(), Some(1)); 237 | assert!(my_cpu_binding.includes(&last_cpu_location)); 238 | } 239 | 240 | proptest! { 241 | #[test] 242 | fn bind_tid_cpu( 243 | set in topology_related_set(Topology::allowed_cpuset) 244 | ) { 245 | // SAFETY: Should always be safe to call 246 | let my_tid = unsafe { libc::gettid() }; 247 | let topology = Topology::test_instance(); 248 | let initial = topology.tid_cpu_binding(my_tid).unwrap(); 249 | 250 | let result = topology.bind_tid_cpu(my_tid, &set); 251 | if result.is_err() { 252 | prop_assert!(!initial.includes(&set) || set.is_empty()); 253 | return Ok(()); 254 | } 255 | 256 | // Linux can enforce a tighter binding than requested 257 | let actual_binding = topology.tid_cpu_binding(my_tid).unwrap(); 258 | prop_assert!( 259 | actual_binding == set 260 | || set.includes(&actual_binding), 261 | "actual binding {actual_binding} doesn't match request {set}" 262 | ); 263 | prop_assert!(set.includes(&topology.last_tid_cpu_location(my_tid).unwrap())); 264 | 265 | topology.bind_tid_cpu(my_tid, &initial).unwrap(); 266 | } 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/interop/mod.rs: -------------------------------------------------------------------------------- 1 | //! Interoperability with other APIs 2 | //! 3 | //! Fundamentally, hwloc is an abstraction layer over OS APIs and a few direct 4 | //! hardware interfaces. Sometimes, hwloc-based software also wants to interact 5 | //! with OS APIs in a different way, and in that case having access to 6 | //! translations of hwloc concepts into the vocabulary of other APIs is useful. 7 | //! This is what the module you're looking at is about. 8 | 9 | #[cfg(any(doc, target_os = "linux"))] 10 | pub mod linux; 11 | #[cfg(any(doc, all(feature = "hwloc-2_5_0", target_os = "windows")))] 12 | pub mod windows; 13 | -------------------------------------------------------------------------------- /src/interop/windows.rs: -------------------------------------------------------------------------------- 1 | //! Windows-specific helpers 2 | 3 | use crate::{ 4 | cpu::cpuset::CpuSet, 5 | errors::{self, RawHwlocError}, 6 | ffi::int, 7 | topology::Topology, 8 | }; 9 | #[allow(unused)] 10 | #[cfg(test)] 11 | use similar_asserts::assert_eq; 12 | use std::{ffi::c_uint, iter::FusedIterator, num::NonZeroUsize}; 13 | 14 | /// # Windows-specific helpers 15 | /// 16 | /// These functions query Windows processor groups. These groups partition the 17 | /// operating system into virtual sets of up to 64 neighbor PUs. Threads and 18 | /// processes may only be bound inside a single group. Although Windows 19 | /// processor groups may be exposed in the hwloc hierarchy as hwloc Groups, 20 | /// they are also often merged into existing hwloc objects such as NUMA nodes 21 | /// or Packages. This API provides explicit information about Windows processor 22 | /// groups so that applications know whether binding to a large set of PUs may 23 | /// fail because it spans over multiple Windows processor groups. 24 | // 25 | // --- Implementation details --- 26 | // 27 | // Upstream docs: https://hwloc.readthedocs.io/en/stable/group__hwlocality__windows.html 28 | impl Topology { 29 | /// Number of Windows processor groups 30 | /// 31 | /// # Errors 32 | /// 33 | /// One reason why this function can fail is if the topology does not match 34 | /// the current system (e.g. loaded from another machine through XML). 35 | #[allow(clippy::missing_errors_doc)] 36 | #[doc(alias = "hwloc_windows_get_nr_processor_groups")] 37 | pub fn num_processor_groups(&self) -> Result { 38 | let count = 39 | // SAFETY: - Topology is trusted to contain a valid ptr (type invariant) 40 | // - hwloc ops are trusted not to modify *const parameters 41 | // - Per documentation, flags must be zero 42 | errors::call_hwloc_positive_or_minus1("hwloc_windows_get_nr_processor_groups", || unsafe { 43 | hwlocality_sys::hwloc_windows_get_nr_processor_groups(self.as_ptr(), 0) 44 | })?; 45 | let count = NonZeroUsize::new(int::expect_usize(count)) 46 | .expect("Unexpected 0 processor group count"); 47 | Ok(count) 48 | } 49 | 50 | /// Enumerate the cpusets of Windows processor groups 51 | /// 52 | /// # Errors 53 | /// 54 | /// One reason why this function can fail is if the topology does not match 55 | /// the current system (e.g. loaded from another machine through XML). 56 | #[allow(clippy::missing_errors_doc)] 57 | #[doc(alias = "hwloc_windows_get_processor_group_cpuset")] 58 | pub fn processor_groups( 59 | &self, 60 | ) -> Result< 61 | impl DoubleEndedIterator> 62 | + Clone 63 | + ExactSizeIterator 64 | + FusedIterator 65 | + '_, 66 | RawHwlocError, 67 | > { 68 | Ok( 69 | (0..usize::from(self.num_processor_groups()?)).map(|pg_index| { 70 | let mut set = CpuSet::new(); 71 | let pg_index = c_uint::try_from(pg_index) 72 | .expect("Can't fail, pg_index upper bound comes from hwloc"); 73 | errors::call_hwloc_zero_or_minus1( 74 | "hwloc_windows_get_processor_group_cpuset", 75 | // SAFETY: - Topology is trusted to contain a valid ptr 76 | // (type invariant) 77 | // - Bitmap is trusted to contain a valid ptr 78 | // (type invariant) 79 | // - hwloc ops are trusted not to modify *const 80 | // parameters 81 | // - hwloc ops are trusted to keep *mut parameters 82 | // in a valid state unless stated otherwise 83 | // - pg_index is in bounds by construction 84 | // - Per documentation, flags must be zero 85 | || unsafe { 86 | hwlocality_sys::hwloc_windows_get_processor_group_cpuset( 87 | self.as_ptr(), 88 | pg_index, 89 | set.as_mut_ptr(), 90 | 0, 91 | ) 92 | }, 93 | )?; 94 | Ok(set) 95 | }), 96 | ) 97 | } 98 | } 99 | 100 | #[cfg(test)] 101 | mod tests { 102 | use super::*; 103 | #[allow(unused)] 104 | use similar_asserts::assert_eq; 105 | 106 | #[test] 107 | fn processor_groups() { 108 | let topology = Topology::test_instance(); 109 | let expected_num_groups = topology.num_processor_groups().unwrap(); 110 | let mut actual_num_groups = 0; 111 | let mut cpuset_union = CpuSet::new(); 112 | for group_cpuset in topology.processor_groups().unwrap() { 113 | cpuset_union |= group_cpuset.unwrap(); 114 | actual_num_groups += 1; 115 | } 116 | assert_eq!(expected_num_groups.get(), actual_num_groups); 117 | assert!(cpuset_union.includes(topology.cpuset())); 118 | assert!(topology.complete_cpuset().includes(&cpuset_union)); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/memory/mod.rs: -------------------------------------------------------------------------------- 1 | //! Memory-specific functionality 2 | //! 3 | //! Most of this module's functionality is exposed via methods of the 4 | //! [`Topology`] struct. The module itself only hosts type definitions that are 5 | //! related to this functionality. 6 | 7 | #[cfg(feature = "hwloc-2_3_0")] 8 | pub mod attribute; 9 | pub mod binding; 10 | pub mod nodeset; 11 | 12 | #[cfg(doc)] 13 | use crate::topology::Topology; 14 | -------------------------------------------------------------------------------- /src/memory/nodeset.rs: -------------------------------------------------------------------------------- 1 | //! NUMA node sets 2 | //! 3 | //! These specialized bitmaps represent sets of NUMA nodes, as exposed by the 4 | //! underlying operating system. 5 | 6 | #[cfg(doc)] 7 | use crate::{bitmap::Bitmap, topology::support::DiscoverySupport}; 8 | use crate::{cpu::cpuset::CpuSet, impl_bitmap_newtype, topology::Topology}; 9 | #[allow(unused)] 10 | #[cfg(test)] 11 | use similar_asserts::assert_eq; 12 | use std::ops::Deref; 13 | 14 | /// # NodeSet-specific API 15 | // 16 | // --- Implementation details --- 17 | // 18 | // This goes before the main impl_bitmap_newtype macro so that it appears before 19 | // the bitmap API reexport in rustdoc. 20 | impl NodeSet { 21 | /// Convert a CPU set into a NUMA node set 22 | /// 23 | /// `cpuset` can be a `&'_ CpuSet` or a `BitmapRef<'_, CpuSet>`. 24 | /// 25 | /// For each PU included in the input `cpuset`, set the corresponding local 26 | /// NUMA node(s) in the output nodeset. 27 | /// 28 | /// If some NUMA nodes have no CPUs at all, this function never sets their 29 | /// indices in the output node set, even if a full CPU set is given in input. 30 | /// 31 | /// Hence the entire topology CPU set, that one can query via 32 | /// [`Topology::cpuset()`], would be converted by this function into the 33 | /// set of all nodes that have some local CPUs. 34 | #[doc(alias = "hwloc_cpuset_to_nodeset")] 35 | pub fn from_cpuset(topology: &Topology, cpuset: impl Deref) -> Self { 36 | /// Polymorphized version of this function (avoids generics code bloat) 37 | fn polymorphized(topology: &Topology, cpuset: &CpuSet) -> NodeSet { 38 | topology 39 | .pus_from_cpuset(cpuset) 40 | .fold(NodeSet::new(), |mut nodeset, pu| { 41 | nodeset |= pu.nodeset().expect("processing units should have nodesets"); 42 | nodeset 43 | }) 44 | } 45 | polymorphized(topology, &cpuset) 46 | } 47 | } 48 | 49 | impl_bitmap_newtype!( 50 | /// A `NodeSet` is a [`Bitmap`] whose bits are set according to NUMA memory 51 | /// node physical OS indexes. 52 | /// 53 | /// Each bit may be converted into a NUMA node object using 54 | /// [`Topology::node_with_os_index()`]. 55 | /// 56 | /// When binding memory on a system without any NUMA node, the single main 57 | /// memory bank is considered as NUMA node #0. 58 | #[doc(alias = "hwloc_nodeset_t")] 59 | #[doc(alias = "hwloc_const_nodeset_t")] 60 | NodeSet 61 | ); 62 | 63 | #[cfg(test)] 64 | mod tests { 65 | use super::*; 66 | use crate::strategies::topology_related_set; 67 | use proptest::prelude::*; 68 | 69 | proptest! { 70 | /// Test for [`NodeSet::from_cpuset()`] 71 | #[test] 72 | fn nodeset_from_cpuset( 73 | cpuset in topology_related_set(Topology::cpuset), 74 | ) { 75 | let topology = Topology::test_instance(); 76 | prop_assert_eq!( 77 | NodeSet::from_cpuset(topology, &cpuset), 78 | topology.pus_from_cpuset(&cpuset) 79 | .map(|pu| pu.nodeset().unwrap().clone_target()) 80 | .reduce(|set1, set2| set1 | set2) 81 | .unwrap_or_default() 82 | ) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/object/attributes/group.rs: -------------------------------------------------------------------------------- 1 | //! [`Group`]-specific attributes 2 | //! 3 | //! [`Group`]: ObjectType::Group 4 | 5 | use crate::ffi::{int, transparent::TransparentNewtype}; 6 | #[cfg(doc)] 7 | use crate::object::types::ObjectType; 8 | use hwlocality_sys::hwloc_group_attr_s; 9 | #[cfg(any(test, feature = "proptest"))] 10 | use proptest::prelude::*; 11 | #[allow(unused)] 12 | #[cfg(test)] 13 | use similar_asserts::assert_eq; 14 | #[cfg(any(test, feature = "proptest", feature = "hwloc-2_3_0"))] 15 | use std::ffi::c_uint; 16 | use std::{ 17 | fmt::{self, Debug}, 18 | hash::Hash, 19 | }; 20 | 21 | /// [`Group`]-specific attributes 22 | /// 23 | /// [`Group`]: ObjectType::Group 24 | #[derive(Copy, Clone, Default, Eq, Hash, PartialEq)] 25 | #[doc(alias = "hwloc_group_attr_s")] 26 | #[doc(alias = "hwloc_obj_attr_u::hwloc_group_attr_s")] 27 | #[repr(transparent)] 28 | pub struct GroupAttributes(hwloc_group_attr_s); 29 | // 30 | impl GroupAttributes { 31 | /// Depth of group object 32 | /// 33 | /// It may change if intermediate Group objects are added. 34 | #[doc(alias = "hwloc_group_attr_s::depth")] 35 | #[doc(alias = "hwloc_obj_attr_u::hwloc_group_attr_s::depth")] 36 | pub fn depth(&self) -> usize { 37 | int::expect_usize(self.0.depth) 38 | } 39 | 40 | /// Internally-used kind of group 41 | #[allow(unused)] 42 | pub(crate) fn kind(&self) -> usize { 43 | int::expect_usize(self.0.kind) 44 | } 45 | 46 | /// Enable group merging and make sure that this group object is discarded 47 | /// in favor of any existing `Group` with the same locality 48 | /// 49 | /// Normally, if the new group has the same locality as an existing group, 50 | /// hwloc keeps the group with the lowest kind value. Given that group kind 51 | /// values are, at the time of writing, an undocumented internal of hwloc, 52 | /// it is bad for group creation behavior to depend the relative kind value 53 | /// of the new group vs other groups, so this method should always be used 54 | /// in the presence of merging for predictable group creation behavior. 55 | #[cfg(feature = "hwloc-2_3_0")] 56 | pub(crate) fn favor_merging(&mut self) { 57 | self.0.kind = c_uint::MAX; 58 | self.0.dont_merge = 0; 59 | } 60 | 61 | /// Internally-used subkind to distinguish different levels of groups with 62 | /// the same kind 63 | #[allow(unused)] 64 | #[doc(alias = "hwloc_group_attr_s::subkind")] 65 | #[doc(alias = "hwloc_obj_attr_u::hwloc_group_attr_s::subkind")] 66 | pub(crate) fn subkind(&self) -> usize { 67 | int::expect_usize(self.0.subkind) 68 | } 69 | 70 | /// Flag preventing groups from being automatically merged with identical 71 | /// parent or children 72 | #[cfg(feature = "hwloc-2_0_4")] 73 | pub fn merging_prevented(&self) -> bool { 74 | #[cfg(not(tarpaulin_include))] 75 | assert!( 76 | self.0.dont_merge == 0 || self.0.dont_merge == 1, 77 | "unexpected hwloc_group_attr_s::dont_merge value" 78 | ); 79 | self.0.dont_merge != 0 80 | } 81 | 82 | /// Tell hwloc that it should not merge this group object with other 83 | /// hierarchically-identical objects. 84 | #[cfg(feature = "hwloc-2_3_0")] 85 | pub(crate) fn prevent_merging(&mut self) { 86 | self.0.dont_merge = 1; 87 | } 88 | } 89 | // 90 | #[cfg(any(test, feature = "proptest"))] 91 | impl Arbitrary for GroupAttributes { 92 | type Parameters = <[c_uint; 3] as Arbitrary>::Parameters; 93 | type Strategy = prop::strategy::Map< 94 | ( 95 | <[c_uint; 3] as Arbitrary>::Strategy, 96 | crate::strategies::HwlocBool, 97 | ), 98 | fn(([c_uint; 3], std::ffi::c_uchar)) -> Self, 99 | >; 100 | 101 | #[allow(unused)] 102 | fn arbitrary_with(args: Self::Parameters) -> Self::Strategy { 103 | let depth_kind_subkind = <[c_uint; 3] as Arbitrary>::arbitrary_with(args); 104 | let dont_merge = crate::strategies::hwloc_bool(); 105 | (depth_kind_subkind, dont_merge).prop_map(|([depth, kind, subkind], dont_merge)| { 106 | Self(hwloc_group_attr_s { 107 | depth, 108 | kind, 109 | subkind, 110 | #[cfg(feature = "hwloc-2_0_4")] 111 | dont_merge, 112 | }) 113 | }) 114 | } 115 | } 116 | // 117 | impl Debug for GroupAttributes { 118 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 119 | let mut debug = f.debug_struct("GroupAttributes"); 120 | 121 | debug.field("depth", &self.depth()); 122 | 123 | #[cfg(feature = "hwloc-2_0_4")] 124 | if self.0.dont_merge <= 1 { 125 | debug.field("merging_prevented", &self.merging_prevented()); 126 | } else { 127 | debug.field("merging_prevented", &format!("{:?}", self.0.dont_merge)); 128 | } 129 | 130 | debug 131 | .field("kind", &self.kind()) 132 | .field("subkind", &self.subkind()) 133 | .finish() 134 | } 135 | } 136 | // 137 | // SAFETY: GroupAttributes is a repr(transparent) newtype of hwloc_group_attr_s 138 | unsafe impl TransparentNewtype for GroupAttributes { 139 | type Inner = hwloc_group_attr_s; 140 | } 141 | 142 | #[cfg(test)] 143 | pub(super) mod tests { 144 | use super::*; 145 | use crate::{ 146 | ffi::transparent::AsInner, 147 | object::{ 148 | attributes::{ 149 | tests::{object_pair, ObjectsWithAttrs}, 150 | ObjectAttributes, 151 | }, 152 | depth::NormalDepth, 153 | types::ObjectType, 154 | TopologyObject, 155 | }, 156 | }; 157 | use hwlocality_sys::hwloc_obj_attr_u; 158 | #[allow(unused)] 159 | use similar_asserts::assert_eq; 160 | use static_assertions::{assert_impl_all, assert_not_impl_any}; 161 | use std::{ 162 | fmt::{Binary, Display, LowerExp, LowerHex, Octal, Pointer, UpperExp, UpperHex}, 163 | io::{self, Read}, 164 | ops::Deref, 165 | panic::UnwindSafe, 166 | }; 167 | 168 | // Check that public types in this module keep implementing all expected 169 | // traits, in the interest of detecting future semver-breaking changes 170 | assert_impl_all!(GroupAttributes: 171 | Copy, Debug, Default, Hash, Sized, Sync, Unpin, UnwindSafe 172 | ); 173 | assert_not_impl_any!(GroupAttributes: 174 | Binary, Deref, Display, Drop, IntoIterator, LowerExp, LowerHex, 175 | Octal, PartialOrd, Pointer, Read, UpperExp, UpperHex, fmt::Write, 176 | io::Write 177 | ); 178 | 179 | #[test] 180 | fn default() -> Result<(), TestCaseError> { 181 | check_any_group(&GroupAttributes::default())?; 182 | Ok(()) 183 | } 184 | 185 | proptest! { 186 | #[test] 187 | fn unary_group(group_attr: GroupAttributes) { 188 | check_any_group(&group_attr)?; 189 | let mut raw = hwloc_obj_attr_u { 190 | group: group_attr.0, 191 | }; 192 | let ptr: *mut hwloc_obj_attr_u = &mut raw; 193 | // SAFETY: Type is consistent with union variant, data is valid 194 | unsafe { 195 | prop_assert!(matches!( 196 | ObjectAttributes::new(ObjectType::Group, &ptr), 197 | Some(ObjectAttributes::Group(attr)) if std::ptr::eq(attr.as_inner(), &raw.group) 198 | )); 199 | } 200 | } 201 | } 202 | 203 | /// Pick a pair of goups in the test topology if possible 204 | fn group_pair() -> impl Strategy> { 205 | let groups = &ObjectsWithAttrs::instance().groups; 206 | object_pair(groups, groups) 207 | } 208 | 209 | proptest! { 210 | /// Check properties that should be true of any pair of groups 211 | #[test] 212 | fn valid_group_pair(group_pair in group_pair()) { 213 | if let Some(pair) = group_pair { 214 | check_valid_group_pair(pair)?; 215 | } 216 | } 217 | } 218 | 219 | /// Check [`GroupAttributes`] properties that should be true of valid 220 | /// [`TopologyObject`]s coming from hwloc 221 | pub(crate) fn check_valid_group(attr: &GroupAttributes) -> Result<(), TestCaseError> { 222 | check_any_group(attr) 223 | } 224 | 225 | /// Check [`GroupAttributes`] properties that should always be true 226 | fn check_any_group(attr: &GroupAttributes) -> Result<(), TestCaseError> { 227 | #[cfg(feature = "hwloc-2_0_4")] 228 | let hwloc_group_attr_s { 229 | depth, 230 | kind, 231 | subkind, 232 | dont_merge, 233 | } = attr.0; 234 | #[cfg(not(feature = "hwloc-2_0_4"))] 235 | let hwloc_group_attr_s { 236 | depth, 237 | kind, 238 | subkind, 239 | } = attr.0; 240 | 241 | prop_assert_eq!(attr.depth(), usize::try_from(depth).unwrap()); 242 | prop_assert_eq!(attr.kind(), usize::try_from(kind).unwrap()); 243 | prop_assert_eq!(attr.subkind(), usize::try_from(subkind).unwrap()); 244 | 245 | #[cfg(feature = "hwloc-2_0_4")] 246 | let merging_prevented_dbg = match dont_merge { 247 | 0 | 1 => { 248 | prop_assert_eq!(attr.merging_prevented(), dont_merge != 0); 249 | format!("merging_prevented: {:?}, ", attr.merging_prevented()) 250 | } 251 | _ => { 252 | crate::tests::assert_panics(|| attr.merging_prevented())?; 253 | format!("merging_prevented: \"{dont_merge:?}\", ") 254 | } 255 | }; 256 | #[cfg(not(feature = "hwloc-2_0_4"))] 257 | let merging_prevented_dbg = String::new(); 258 | 259 | prop_assert_eq!( 260 | format!("{attr:?}"), 261 | format!( 262 | "GroupAttributes {{ \ 263 | depth: {:?}, \ 264 | {}\ 265 | kind: {:?}, \ 266 | subkind: {:?} \ 267 | }}", 268 | attr.depth(), 269 | merging_prevented_dbg, 270 | attr.kind(), 271 | attr.subkind(), 272 | ) 273 | ); 274 | 275 | #[cfg(feature = "hwloc-2_3_0")] 276 | { 277 | let mut buf = *attr; 278 | let mut expected = *attr; 279 | 280 | buf.prevent_merging(); 281 | expected.0.dont_merge = 1; 282 | prop_assert_eq!(buf, expected); 283 | prop_assert!(buf.merging_prevented()); 284 | 285 | buf.favor_merging(); 286 | expected.0.dont_merge = 0; 287 | prop_assert!(buf.0.kind > 0); 288 | expected.0.kind = buf.0.kind; 289 | prop_assert_eq!(buf, expected); 290 | prop_assert!(!buf.merging_prevented()); 291 | } 292 | 293 | Ok(()) 294 | } 295 | 296 | /// Check attribute properties that should be true of any pair of valid group 297 | /// objects from the hwloc topology 298 | fn check_valid_group_pair([group1, group2]: [&TopologyObject; 2]) -> Result<(), TestCaseError> { 299 | fn group_depth( 300 | group: &TopologyObject, 301 | ) -> Result<(NormalDepth, GroupAttributes), TestCaseError> { 302 | let res = if let Some(ObjectAttributes::Group(attr)) = group.attributes() { 303 | (group.depth().expect_normal(), *attr) 304 | } else { 305 | prop_assert!(false, "Not a group"); 306 | unreachable!() 307 | }; 308 | Ok(res) 309 | } 310 | let (depth1, attr1) = group_depth(group1)?; 311 | let (depth2, attr2) = group_depth(group2)?; 312 | 313 | prop_assert_eq!(depth1.cmp(&depth2), attr1.depth().cmp(&attr2.depth())); 314 | 315 | Ok(()) 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /src/object/attributes/osdev.rs: -------------------------------------------------------------------------------- 1 | //! [`OSDevice`]-specific attributes 2 | //! 3 | //! [`OSDevice`]: ObjectType::OSDevice 4 | 5 | #[cfg(doc)] 6 | use crate::object::types::ObjectType; 7 | use crate::{ffi::transparent::TransparentNewtype, object::types::OSDeviceType}; 8 | use hwlocality_sys::hwloc_osdev_attr_s; 9 | #[cfg(any(test, feature = "proptest"))] 10 | use proptest::prelude::*; 11 | #[allow(unused)] 12 | #[cfg(test)] 13 | use similar_asserts::assert_eq; 14 | use std::{ 15 | fmt::{self, Debug}, 16 | hash::Hash, 17 | }; 18 | 19 | /// [`OSDevice`]-specific attributes 20 | /// 21 | /// [`OSDevice`]: ObjectType::OSDevice 22 | #[derive(Copy, Clone, Eq, Hash, PartialEq)] 23 | #[doc(alias = "hwloc_osdev_attr_s")] 24 | #[doc(alias = "hwloc_obj_attr_u::hwloc_osdev_attr_s")] 25 | #[repr(transparent)] 26 | pub struct OSDeviceAttributes(pub(super) hwloc_osdev_attr_s); 27 | // 28 | impl OSDeviceAttributes { 29 | /// OS device type 30 | #[doc(alias = "hwloc_osdev_attr_s::type")] 31 | #[doc(alias = "hwloc_obj_attr_u::hwloc_osdev_attr_s::type")] 32 | pub fn device_type(&self) -> OSDeviceType { 33 | // SAFETY: OS device attributes are not user-editable so we are sure 34 | // this value comes from hwloc 35 | unsafe { OSDeviceType::from_hwloc(self.0.ty) } 36 | } 37 | } 38 | // 39 | #[cfg(any(test, feature = "proptest"))] 40 | impl Arbitrary for OSDeviceAttributes { 41 | type Parameters = (); 42 | type Strategy = prop::strategy::Map< 43 | crate::strategies::EnumRepr, 44 | fn(hwlocality_sys::hwloc_obj_osdev_type_t) -> Self, 45 | >; 46 | 47 | fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { 48 | use hwlocality_sys::hwloc_obj_osdev_type_t; 49 | crate::strategies::enum_repr::() 50 | .prop_map(|ty| Self(hwloc_osdev_attr_s { ty })) 51 | } 52 | } 53 | // 54 | impl Debug for OSDeviceAttributes { 55 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 56 | f.debug_struct("OSDeviceAttributes") 57 | .field("device_type", &self.device_type()) 58 | .finish() 59 | } 60 | } 61 | // 62 | // SAFETY: OSDeviceAttributes is a repr(transparent) newtype of hwloc_osdev_attr_s 63 | unsafe impl TransparentNewtype for OSDeviceAttributes { 64 | type Inner = hwloc_osdev_attr_s; 65 | } 66 | 67 | #[cfg(test)] 68 | pub(super) mod tests { 69 | use super::*; 70 | use crate::{ 71 | ffi::transparent::AsInner, 72 | object::{attributes::ObjectAttributes, types::ObjectType}, 73 | }; 74 | use hwlocality_sys::hwloc_obj_attr_u; 75 | #[allow(unused)] 76 | use similar_asserts::assert_eq; 77 | use static_assertions::{assert_impl_all, assert_not_impl_any}; 78 | use std::{ 79 | fmt::{Binary, Display, LowerExp, LowerHex, Octal, Pointer, UpperExp, UpperHex}, 80 | io::{self, Read}, 81 | ops::Deref, 82 | panic::UnwindSafe, 83 | }; 84 | 85 | // Check that public types in this module keep implementing all expected 86 | // traits, in the interest of detecting future semver-breaking changes 87 | assert_impl_all!(OSDeviceAttributes: 88 | Copy, Debug, Hash, Sized, Sync, Unpin, UnwindSafe 89 | ); 90 | assert_not_impl_any!(OSDeviceAttributes: 91 | Binary, Deref, Default, Display, Drop, IntoIterator, LowerExp, LowerHex, 92 | Octal, PartialOrd, Pointer, Read, UpperExp, UpperHex, fmt::Write, 93 | io::Write 94 | ); 95 | 96 | proptest! { 97 | #[test] 98 | fn unary_osdev(osdev_attr: OSDeviceAttributes) { 99 | check_any_osdev(&osdev_attr)?; 100 | let mut raw = hwloc_obj_attr_u { 101 | osdev: osdev_attr.0, 102 | }; 103 | let ptr: *mut hwloc_obj_attr_u = &mut raw; 104 | // SAFETY: Type is consistent with union variant, data is valid 105 | unsafe { 106 | prop_assert!(matches!( 107 | ObjectAttributes::new(ObjectType::OSDevice, &ptr), 108 | Some(ObjectAttributes::OSDevice(attr)) if std::ptr::eq(attr.as_inner(), &raw.osdev) 109 | )); 110 | } 111 | } 112 | } 113 | 114 | /// Check [`OSDeviceAttributes`] properties that should be true of valid 115 | /// [`TopologyObject`]s coming from hwloc 116 | #[allow(clippy::trivially_copy_pass_by_ref)] 117 | pub(crate) fn check_valid_osdev(attr: &OSDeviceAttributes) -> Result<(), TestCaseError> { 118 | check_any_osdev(attr) 119 | } 120 | 121 | /// Check [`OSDeviceAttributes`] properties that should always be true 122 | #[allow(clippy::option_if_let_else, clippy::trivially_copy_pass_by_ref)] 123 | fn check_any_osdev(attr: &OSDeviceAttributes) -> Result<(), TestCaseError> { 124 | let device_type_dbg = format!("{:?}", attr.device_type()); 125 | prop_assert_eq!( 126 | format!("{attr:?}"), 127 | format!("OSDeviceAttributes {{ device_type: {device_type_dbg} }}") 128 | ); 129 | Ok(()) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/object/attributes/pci.rs: -------------------------------------------------------------------------------- 1 | //! [`PCIDevice`]-specific attributes 2 | //! 3 | //! [`PCIDevice`]: ObjectType::PCIDevice 4 | 5 | use crate::ffi::transparent::TransparentNewtype; 6 | #[cfg(doc)] 7 | use crate::object::types::ObjectType; 8 | use hwlocality_sys::hwloc_pcidev_attr_s; 9 | #[cfg(any(test, feature = "proptest"))] 10 | use proptest::prelude::*; 11 | #[allow(unused)] 12 | #[cfg(test)] 13 | use similar_asserts::assert_eq; 14 | use std::fmt::Debug; 15 | 16 | /// PCI domain width (depends on hwloc version) 17 | #[cfg(feature = "hwloc-3_0_0")] 18 | #[cfg_attr(docsrs, doc(cfg(all())))] 19 | pub type PCIDomain = u32; 20 | 21 | /// PCI domain width (depends on hwloc version) 22 | #[cfg(not(feature = "hwloc-3_0_0"))] 23 | #[cfg_attr(docsrs, doc(cfg(all())))] 24 | pub type PCIDomain = u16; 25 | 26 | /// [`PCIDevice`]-specific attributes 27 | /// 28 | /// [`PCIDevice`]: ObjectType::PCIDevice 29 | #[derive(Copy, Clone, Debug, Default, PartialEq)] 30 | #[doc(alias = "hwloc_pcidev_attr_s")] 31 | #[doc(alias = "hwloc_obj_attr_u::hwloc_pcidev_attr_s")] 32 | #[repr(transparent)] 33 | pub struct PCIDeviceAttributes(pub(super) hwloc_pcidev_attr_s); 34 | // 35 | impl PCIDeviceAttributes { 36 | /// PCI domain 37 | #[doc(alias = "hwloc_pcidev_attr_s::domain")] 38 | #[doc(alias = "hwloc_obj_attr_u::hwloc_pcidev_attr_s::domain")] 39 | pub fn domain(&self) -> PCIDomain { 40 | self.0.domain 41 | } 42 | 43 | /// PCI bus id 44 | #[doc(alias = "hwloc_pcidev_attr_s::bus")] 45 | #[doc(alias = "hwloc_obj_attr_u::hwloc_pcidev_attr_s::bus")] 46 | pub fn bus_id(&self) -> u8 { 47 | self.0.bus 48 | } 49 | 50 | /// PCI bus device 51 | #[doc(alias = "hwloc_pcidev_attr_s::dev")] 52 | #[doc(alias = "hwloc_obj_attr_u::hwloc_pcidev_attr_s::dev")] 53 | pub fn bus_device(&self) -> u8 { 54 | self.0.dev 55 | } 56 | 57 | /// PCI function 58 | #[doc(alias = "hwloc_pcidev_attr_s::func")] 59 | #[doc(alias = "hwloc_obj_attr_u::hwloc_pcidev_attr_s::func")] 60 | pub fn function(&self) -> u8 { 61 | self.0.func 62 | } 63 | 64 | /// PCI class ID 65 | #[doc(alias = "hwloc_pcidev_attr_s::class_id")] 66 | #[doc(alias = "hwloc_obj_attr_u::hwloc_pcidev_attr_s::class_id")] 67 | pub fn class_id(&self) -> u16 { 68 | self.0.class_id 69 | } 70 | 71 | /// PCI vendor ID 72 | #[doc(alias = "hwloc_pcidev_attr_s::vendor_id")] 73 | #[doc(alias = "hwloc_obj_attr_u::hwloc_pcidev_attr_s::vendor_id")] 74 | pub fn vendor_id(&self) -> u16 { 75 | self.0.vendor_id 76 | } 77 | 78 | /// PCI device ID 79 | #[doc(alias = "hwloc_pcidev_attr_s::device_id")] 80 | #[doc(alias = "hwloc_obj_attr_u::hwloc_pcidev_attr_s::device_id")] 81 | pub fn device_id(&self) -> u16 { 82 | self.0.device_id 83 | } 84 | 85 | /// PCI sub-vendor ID 86 | #[doc(alias = "hwloc_pcidev_attr_s::subvendor_id")] 87 | #[doc(alias = "hwloc_obj_attr_u::hwloc_pcidev_attr_s::subvendor_id")] 88 | pub fn subvendor_id(&self) -> u16 { 89 | self.0.subvendor_id 90 | } 91 | 92 | /// PCI sub-device ID 93 | #[doc(alias = "hwloc_pcidev_attr_s::subdevice_id")] 94 | #[doc(alias = "hwloc_obj_attr_u::hwloc_pcidev_attr_s::subdevice_id")] 95 | pub fn subdevice_id(&self) -> u16 { 96 | self.0.subdevice_id 97 | } 98 | 99 | /// PCI revision 100 | #[doc(alias = "hwloc_pcidev_attr_s::revision")] 101 | #[doc(alias = "hwloc_obj_attr_u::hwloc_pcidev_attr_s::revision")] 102 | pub fn revision(&self) -> u8 { 103 | self.0.revision 104 | } 105 | 106 | /// Link speed in GB/s 107 | #[doc(alias = "hwloc_pcidev_attr_s::linkspeed")] 108 | #[doc(alias = "hwloc_obj_attr_u::hwloc_pcidev_attr_s::linkspeed")] 109 | pub fn link_speed(&self) -> f32 { 110 | self.0.linkspeed 111 | } 112 | } 113 | // 114 | #[cfg(any(test, feature = "proptest"))] 115 | impl Arbitrary for PCIDeviceAttributes { 116 | type Parameters = <(PCIDomain, [u8; 4], [u16; 5]) as Arbitrary>::Parameters; 117 | type Strategy = prop::strategy::Map< 118 | ( 119 | <(PCIDomain, [u8; 4], [u16; 5]) as Arbitrary>::Strategy, 120 | prop::num::f32::Any, 121 | ), 122 | fn(((PCIDomain, [u8; 4], [u16; 5]), f32)) -> Self, 123 | >; 124 | 125 | #[allow(unused)] 126 | fn arbitrary_with(args: Self::Parameters) -> Self::Strategy { 127 | ( 128 | <(PCIDomain, [u8; 4], [u16; 5])>::arbitrary_with(args), 129 | prop::num::f32::ANY, 130 | ) 131 | .prop_map( 132 | |( 133 | ( 134 | domain, 135 | [bus, dev, func, revision], 136 | [class_id, vendor_id, device_id, subvendor_id, subdevice_id], 137 | ), 138 | linkspeed, 139 | )| { 140 | Self(hwloc_pcidev_attr_s { 141 | domain, 142 | bus, 143 | dev, 144 | func, 145 | class_id, 146 | vendor_id, 147 | device_id, 148 | subvendor_id, 149 | subdevice_id, 150 | revision, 151 | linkspeed, 152 | }) 153 | }, 154 | ) 155 | } 156 | } 157 | // 158 | // SAFETY: PCIDeviceAttributes is a repr(transparent) newtype of hwloc_pcidev_attr_s 159 | unsafe impl TransparentNewtype for PCIDeviceAttributes { 160 | type Inner = hwloc_pcidev_attr_s; 161 | } 162 | 163 | #[cfg(test)] 164 | pub(super) mod tests { 165 | use super::*; 166 | use crate::{ 167 | ffi::transparent::AsInner, 168 | object::{ 169 | attributes::{ 170 | tests::{object_pair, parent_child, ObjectsWithAttrs}, 171 | ObjectAttributes, 172 | }, 173 | types::ObjectType, 174 | TopologyObject, 175 | }, 176 | }; 177 | use hwlocality_sys::hwloc_obj_attr_u; 178 | #[allow(unused)] 179 | use similar_asserts::assert_eq; 180 | use static_assertions::{assert_impl_all, assert_not_impl_any}; 181 | use std::{ 182 | error::Error, 183 | fmt::{self, Binary, Display, LowerExp, LowerHex, Octal, Pointer, UpperExp, UpperHex}, 184 | hash::Hash, 185 | io::{self, Read}, 186 | iter::{Product, Sum}, 187 | ops::{ 188 | Add, AddAssign, BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Deref, 189 | Div, DivAssign, Mul, MulAssign, Not, Rem, RemAssign, Shl, ShlAssign, Shr, ShrAssign, 190 | Sub, SubAssign, 191 | }, 192 | panic::UnwindSafe, 193 | str::FromStr, 194 | }; 195 | 196 | // Check that public types in this module keep implementing all expected 197 | // traits, in the interest of detecting future semver-breaking changes 198 | assert_impl_all!(PCIDeviceAttributes: 199 | Copy, Debug, Default, PartialEq, Sized, Sync, Unpin, UnwindSafe 200 | ); 201 | assert_not_impl_any!(PCIDeviceAttributes: 202 | Binary, Deref, Display, Drop, Eq, IntoIterator, LowerExp, LowerHex, 203 | Octal, PartialOrd, Pointer, Read, UpperExp, UpperHex, fmt::Write, 204 | io::Write 205 | ); 206 | assert_impl_all!(PCIDomain: 207 | Add, AddAssign, Binary, BitAnd, BitAndAssign, BitOr, BitOrAssign, 208 | BitXor, BitXorAssign, Copy, Debug, Default, Display, Div, DivAssign, 209 | FromStr, Hash, LowerExp, LowerHex, Mul, MulAssign, Not, Octal, Ord, 210 | Product, Sized, Rem, RemAssign, Shl, ShlAssign, Shr, ShrAssign, Sub, 211 | SubAssign, Sum, Sync, 212 | TryFrom, TryFrom, 213 | TryFrom, TryFrom, 214 | TryFrom, TryFrom, 215 | TryFrom, TryFrom, 216 | TryFrom, TryFrom, 217 | TryFrom, TryFrom, 218 | TryInto, TryInto, 219 | TryInto, TryInto, 220 | TryInto, TryInto, 221 | TryInto, TryInto, 222 | TryInto, TryInto, 223 | TryInto, TryInto, 224 | Unpin, UnwindSafe, UpperExp, UpperHex 225 | ); 226 | assert_not_impl_any!(PCIDomain: 227 | Deref, Drop, Error, IntoIterator, Pointer, Read, fmt::Write, io::Write 228 | ); 229 | 230 | #[test] 231 | fn default() -> Result<(), TestCaseError> { 232 | check_any_pci(&PCIDeviceAttributes::default())?; 233 | Ok(()) 234 | } 235 | 236 | proptest! { 237 | #[test] 238 | fn unary_pci(pcidev_attr: PCIDeviceAttributes) { 239 | check_any_pci(&pcidev_attr)?; 240 | let mut raw = hwloc_obj_attr_u { 241 | pcidev: pcidev_attr.0, 242 | }; 243 | let ptr: *mut hwloc_obj_attr_u = &mut raw; 244 | // SAFETY: Type is consistent with union variant, data is valid 245 | unsafe { 246 | prop_assert!(matches!( 247 | ObjectAttributes::new(ObjectType::PCIDevice, &ptr), 248 | Some(ObjectAttributes::PCIDevice(attr)) if std::ptr::eq(attr.as_inner(), &raw.pcidev) 249 | )); 250 | } 251 | } 252 | } 253 | 254 | /// Pick a pair of PCI devices in the test topology if possible 255 | fn pci_pair() -> impl Strategy> { 256 | let pci_devices = &ObjectsWithAttrs::instance().pci_devices; 257 | object_pair(pci_devices, pci_devices) 258 | } 259 | 260 | proptest! { 261 | /// Check properties that should be true of any pair of pcis 262 | #[test] 263 | fn valid_pci_pair(pci_pair in pci_pair()) { 264 | if let Some(pair) = pci_pair { 265 | check_valid_pci_pair(pair)?; 266 | } 267 | } 268 | } 269 | 270 | /// Check [`PCIDeviceAttributes`] properties that should be true of valid 271 | /// [`TopologyObject`]s coming from hwloc 272 | #[allow( 273 | clippy::cast_possible_truncation, 274 | clippy::cast_precision_loss, 275 | clippy::cast_sign_loss 276 | )] 277 | pub(crate) fn check_valid_pci(attr: &PCIDeviceAttributes) -> Result<(), TestCaseError> { 278 | check_any_pci(attr)?; 279 | 280 | let link_speed = attr.link_speed(); 281 | prop_assert!( 282 | link_speed.is_finite() && link_speed >= 0.0 && link_speed < f32::from(u16::MAX) 283 | ); 284 | 285 | Ok(()) 286 | } 287 | 288 | /// Check [`PCIDeviceAttributes`] properties that should always be true 289 | pub(crate) fn check_any_pci(attr: &PCIDeviceAttributes) -> Result<(), TestCaseError> { 290 | let hwloc_pcidev_attr_s { 291 | domain, 292 | bus, 293 | dev, 294 | func, 295 | class_id, 296 | vendor_id, 297 | device_id, 298 | subvendor_id, 299 | subdevice_id, 300 | revision, 301 | linkspeed, 302 | } = attr.0; 303 | prop_assert_eq!(attr.domain(), domain); 304 | prop_assert_eq!(attr.bus_id(), bus); 305 | prop_assert_eq!(attr.bus_device(), dev); 306 | prop_assert_eq!(attr.function(), func); 307 | prop_assert_eq!(attr.class_id(), class_id); 308 | prop_assert_eq!(attr.device_id(), device_id); 309 | prop_assert_eq!(attr.vendor_id(), vendor_id); 310 | prop_assert_eq!(attr.subvendor_id(), subvendor_id); 311 | prop_assert_eq!(attr.subdevice_id(), subdevice_id); 312 | prop_assert_eq!(attr.revision(), revision); 313 | prop_assert_eq!(attr.link_speed().to_bits(), linkspeed.to_bits()); 314 | Ok(()) 315 | } 316 | 317 | /// Check attribute properties that should be true of any pair of valid PCI 318 | /// objects from the hwloc topology 319 | fn check_valid_pci_pair([pci1, pci2]: [&TopologyObject; 2]) -> Result<(), TestCaseError> { 320 | let attr1 = pci_attributes(pci1)?; 321 | let attr2 = pci_attributes(pci2)?; 322 | 323 | let parent_child_attr = parent_child([(pci1, attr1), (pci2, attr2)]); 324 | if let Some([parent, child]) = parent_child_attr { 325 | prop_assert!(child.revision() <= parent.revision()); 326 | prop_assert!(child.link_speed() <= parent.link_speed()); 327 | } 328 | 329 | Ok(()) 330 | } 331 | 332 | /// Extract the PCI attributes from a PCI device object 333 | pub(crate) fn pci_attributes( 334 | pci: &TopologyObject, 335 | ) -> Result { 336 | let res = if let Some(ObjectAttributes::PCIDevice(attr)) = pci.attributes() { 337 | *attr 338 | } else { 339 | prop_assert!(false, "Not a PCI device"); 340 | unreachable!() 341 | }; 342 | Ok(res) 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /src/object/lists.rs: -------------------------------------------------------------------------------- 1 | //! Full lists of objects contained within the topology 2 | 3 | use super::{ 4 | depth::{Depth, NormalDepth}, 5 | TopologyObject, 6 | }; 7 | use crate::topology::Topology; 8 | #[allow(unused)] 9 | #[cfg(test)] 10 | use similar_asserts::assert_eq; 11 | use std::iter::FusedIterator; 12 | #[cfg(test)] 13 | use std::sync::OnceLock; 14 | 15 | /// # Full object lists 16 | /// 17 | /// For some use cases, especially testing, it is convenient to have a full list 18 | /// of all objects contained within a topology. These methods provide just that. 19 | /// 20 | /// This functionality is unique to the Rust hwloc bindings 21 | impl Topology { 22 | /// Full list of objects in the topology, first normal objects ordered by 23 | /// increasing depth then virtual objects ordered by type 24 | pub fn objects(&self) -> impl FusedIterator + Clone { 25 | self.normal_objects().chain(self.virtual_objects()) 26 | } 27 | 28 | /// Pre-computed list of objects from the test instance 29 | #[cfg(test)] 30 | pub(crate) fn test_objects() -> &'static [&'static TopologyObject] { 31 | static OBJECTS: OnceLock> = OnceLock::new(); 32 | &OBJECTS.get_or_init(|| Self::test_instance().objects().collect())[..] 33 | } 34 | 35 | /// Like [`Topology::test_objects()`], but for the foreign instance 36 | #[cfg(all(test, feature = "hwloc-2_3_0"))] 37 | pub(crate) fn foreign_objects() -> &'static [&'static TopologyObject] { 38 | static OBJECTS: OnceLock> = OnceLock::new(); 39 | &OBJECTS.get_or_init(|| Self::foreign_instance().objects().collect())[..] 40 | } 41 | 42 | /// Full list of objects contains in the normal hierarchy of the topology, 43 | /// ordered by increasing depth 44 | pub fn normal_objects(&self) -> impl FusedIterator + Clone { 45 | NormalDepth::iter_range(NormalDepth::MIN, self.depth()) 46 | .flat_map(|depth| self.objects_at_depth(depth)) 47 | } 48 | 49 | /// Full list of virtual objects in the topology, ordered by type 50 | pub fn virtual_objects(&self) -> impl FusedIterator + Clone { 51 | Depth::VIRTUAL_DEPTHS 52 | .iter() 53 | .flat_map(|&depth| self.objects_at_depth(depth)) 54 | } 55 | 56 | /// Full list of memory objects in the topology, ordered by type 57 | pub fn memory_objects(&self) -> impl FusedIterator + Clone { 58 | Depth::MEMORY_DEPTHS 59 | .iter() 60 | .flat_map(|&depth| self.objects_at_depth(depth)) 61 | } 62 | 63 | /// Full list of I/O objects in the topology, ordered by type 64 | pub fn io_objects(&self) -> impl FusedIterator + Clone { 65 | Depth::IO_DEPTHS 66 | .iter() 67 | .flat_map(|&depth| self.objects_at_depth(depth)) 68 | } 69 | } 70 | 71 | #[cfg(test)] 72 | pub(crate) mod tests { 73 | use super::*; 74 | use crate::object::ObjectType; 75 | use proptest::prelude::*; 76 | use similar_asserts::assert_eq; 77 | use std::collections::{HashMap, HashSet}; 78 | 79 | /// Key objects by their global persistent ID and check for ID uniqueness 80 | pub(crate) fn checked_object_set<'a>( 81 | it: impl Iterator, 82 | ) -> HashMap { 83 | let mut set = HashMap::new(); 84 | for obj in it { 85 | assert!( 86 | set.insert(obj.global_persistent_index(), obj).is_none(), 87 | "global_persistent_index should be unique across the topology" 88 | ); 89 | } 90 | set 91 | } 92 | 93 | /// Extract the keys from the output of [`checked_object_set()`] 94 | pub(crate) fn object_ids_from_set(map: &HashMap) -> HashSet { 95 | map.keys().copied().collect() 96 | } 97 | 98 | /// Compare the output of two iterators of objects without duplicates 99 | pub(crate) fn compare_object_sets<'a>( 100 | result: impl Iterator, 101 | reference: impl Iterator, 102 | ) -> Result<(), TestCaseError> { 103 | prop_assert_eq!( 104 | object_ids_from_set(&checked_object_set(result)), 105 | object_ids_from_set(&checked_object_set(reference)) 106 | ); 107 | Ok(()) 108 | } 109 | 110 | /// Check that the various object lists match their definitions 111 | #[test] 112 | fn object_lists() { 113 | let topology = Topology::test_instance(); 114 | 115 | let objects = checked_object_set(topology.objects()); 116 | let keys = object_ids_from_set(&objects); 117 | 118 | let normal_objects = checked_object_set(topology.normal_objects()); 119 | assert!(normal_objects 120 | .values() 121 | .all(|obj| obj.object_type().is_normal())); 122 | let normal_keys = object_ids_from_set(&normal_objects); 123 | 124 | let virtual_objects = checked_object_set(topology.virtual_objects()); 125 | assert!(virtual_objects 126 | .values() 127 | .all(|obj| !obj.object_type().is_normal())); 128 | let virtual_keys = object_ids_from_set(&virtual_objects); 129 | 130 | assert_eq!(keys, &normal_keys | &virtual_keys); 131 | assert_eq!(normal_keys, &keys - &virtual_keys); 132 | assert_eq!(virtual_keys, &keys - &normal_keys); 133 | 134 | let memory_objects = checked_object_set(topology.memory_objects()); 135 | assert!(memory_objects 136 | .values() 137 | .all(|obj| obj.object_type().is_memory())); 138 | let memory_keys = object_ids_from_set(&memory_objects); 139 | 140 | let io_objects = checked_object_set(topology.io_objects()); 141 | assert!(io_objects.values().all(|obj| obj.object_type().is_io())); 142 | let io_keys = object_ids_from_set(&io_objects); 143 | 144 | let misc_objects = checked_object_set(topology.objects_with_type(ObjectType::Misc)); 145 | assert!(misc_objects 146 | .values() 147 | .all(|obj| obj.object_type() == ObjectType::Misc)); 148 | let misc_keys = object_ids_from_set(&misc_objects); 149 | 150 | assert_eq!(virtual_keys, &(&memory_keys | &io_keys) | &misc_keys); 151 | assert_eq!(memory_keys, &(&virtual_keys - &io_keys) - &misc_keys); 152 | assert_eq!(io_keys, &(&virtual_keys - &memory_keys) - &misc_keys); 153 | assert_eq!(misc_keys, &(&virtual_keys - &memory_keys) - &io_keys); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/path.rs: -------------------------------------------------------------------------------- 1 | //! File path handling 2 | //! 3 | //! Several hwloc methods ingest file paths. Conversion from Rust file paths to 4 | //! C file paths can fail in several way, and this module is concerned with the 5 | //! associated error detection and reporting. 6 | 7 | use crate::{errors::NulError, ffi::string::LibcString}; 8 | #[allow(unused)] 9 | #[cfg(test)] 10 | use similar_asserts::assert_eq; 11 | use std::path::Path; 12 | use thiserror::Error; 13 | 14 | /// Requested file path is not suitable for hwloc consumption 15 | #[derive(Copy, Clone, Debug, Error, Eq, Hash, Ord, PartialEq, PartialOrd)] 16 | pub enum PathError { 17 | /// Path contains the NUL char, and is thus not compatible with C 18 | #[error("hwloc file paths can't contain NUL chars")] 19 | ContainsNul, 20 | 21 | /// Path contains non-Unicode data 22 | /// 23 | /// We need paths to be valid Unicode, even though most operating systems do 24 | /// not mandate it, because that is a prerequisite for portably converting 25 | /// paths to `char*` for C/hwloc consumption. 26 | #[error("hwloc file paths can't contain non-Unicode data")] 27 | NotUnicode, 28 | } 29 | // 30 | impl From for PathError { 31 | fn from(_: NulError) -> Self { 32 | Self::ContainsNul 33 | } 34 | } 35 | 36 | /// Convert a file path into something that hwloc can ingest, or die trying 37 | /// 38 | /// # Errors 39 | /// 40 | /// - [`ContainsNul`] if `path` contains NUL chars. 41 | /// - [`NotUnicode`] if `path` contains non-Unicode data 42 | pub(crate) fn make_hwloc_path(path: impl AsRef) -> Result { 43 | /// Polymorphized version of this function (avoids generics code bloat) 44 | fn polymorphized(path: &Path) -> Result { 45 | Ok(LibcString::new( 46 | path.to_str().ok_or(PathError::NotUnicode)?, 47 | )?) 48 | } 49 | polymorphized(path.as_ref()) 50 | } 51 | 52 | #[cfg(test)] 53 | mod tests { 54 | use super::*; 55 | use proptest::{path::PathParams, prelude::*}; 56 | #[allow(unused)] 57 | use similar_asserts::assert_eq; 58 | use static_assertions::{assert_impl_all, assert_not_impl_any}; 59 | use std::{ 60 | error::Error, 61 | fmt::{self, Binary, LowerExp, LowerHex, Octal, Pointer, UpperExp, UpperHex}, 62 | hash::Hash, 63 | io::{self, Read}, 64 | ops::Deref, 65 | panic::UnwindSafe, 66 | path::PathBuf, 67 | }; 68 | 69 | // Check that public types in this module keep implementing all expected 70 | // traits, in the interest of detecting future semver-breaking changes 71 | assert_impl_all!(PathError: 72 | Copy, Error, From, Hash, Ord, Sized, Sync, Unpin, UnwindSafe 73 | ); 74 | assert_not_impl_any!(PathError: 75 | Binary, Default, Deref, Drop, IntoIterator, LowerExp, LowerHex, Octal, 76 | Pointer, Read, UpperExp, UpperHex, fmt::Write, io::Write 77 | ); 78 | 79 | /// Path generator that's actually exhaustive, unlike proptest's default 80 | fn any_path() -> impl Strategy { 81 | PathBuf::arbitrary_with(PathParams::default().with_component_regex(".*")) 82 | } 83 | 84 | proptest! { 85 | #[allow(clippy::option_if_let_else)] 86 | #[test] 87 | fn make_hwloc_path(path in any_path()) { 88 | let res = super::make_hwloc_path(&path); 89 | if let Some(s) = path.to_str() { 90 | if s.contains('\0') { 91 | prop_assert_eq!(res, Err(PathError::ContainsNul)); 92 | } else { 93 | prop_assert_eq!(res.as_ref().map(AsRef::as_ref), Ok(s)); 94 | } 95 | } else { 96 | prop_assert_eq!(res, Err(PathError::NotUnicode)) 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/strategies.rs: -------------------------------------------------------------------------------- 1 | //! Common strategies for property-based testing 2 | //! 3 | //! This is a place for centralizing proptest strategies which are more clever 4 | //! than an [`Arbitrary`] and don't have one clear place to go in the codebase. 5 | 6 | use crate::bitmap::BitmapIndex; 7 | #[cfg(test)] 8 | use crate::{ 9 | bitmap::{Bitmap, BitmapRef, SpecializedBitmap, SpecializedBitmapRef}, 10 | object::TopologyObject, 11 | topology::Topology, 12 | }; 13 | use proptest::{ 14 | collection::SizeRange, 15 | prelude::*, 16 | sample::Select, 17 | strategy::{Map, TupleUnion, WA}, 18 | string::RegexGeneratorStrategy, 19 | }; 20 | use std::{ 21 | ffi::{c_uchar, c_uint}, 22 | fmt::Debug, 23 | ops::{Range, RangeInclusive}, 24 | }; 25 | use strum::IntoEnumIterator; 26 | 27 | /// String generator that's actually exhaustive, unlike proptest's default 28 | pub(crate) fn any_string() -> AnyString { 29 | prop::string::string_regex(".*").expect("this is a valid regex") 30 | } 31 | 32 | /// Strategy emitted by [`any_string()`] 33 | pub(crate) type AnyString = RegexGeneratorStrategy; 34 | 35 | /// Generate an hwloc boolean with reasonable valid state probability 36 | pub(crate) fn hwloc_bool() -> HwlocBool { 37 | prop_oneof![ 38 | 4 => prop::bool::ANY.prop_map(c_uchar::from), 39 | 1 => 2..=c_uchar::MAX, 40 | ] 41 | } 42 | 43 | /// Strategy emitted by [`hwloc_bool()`] 44 | pub(crate) type HwlocBool = TupleUnion<( 45 | WA c_uchar>>, 46 | WA>, 47 | )>; 48 | 49 | /// Generate an integer where the 0 value is special 50 | pub(crate) fn int_special0(zero: Int, one: Int, max: Int) -> IntSpecial0 51 | where 52 | RangeInclusive: Strategy, 53 | { 54 | prop_oneof![ 55 | 4 => one..=max, 56 | 1 => Just(zero) 57 | ] 58 | } 59 | 60 | /// Strategy emitted by [`int_special0()`] 61 | pub(crate) type IntSpecial0 = TupleUnion<(WA>, WA>)>; 62 | 63 | /// Specialization of [`int_special0()`] for [`u64`] 64 | pub(crate) fn u64_special0() -> IntSpecial0 { 65 | int_special0(0, 1, u64::MAX) 66 | } 67 | 68 | /// Specialization of [`int_special0()`] for [`c_uint`] 69 | pub(crate) fn uint_special0() -> IntSpecial0 { 70 | int_special0(0, 1, c_uint::MAX) 71 | } 72 | 73 | /// Generate an hwloc enum repr 74 | pub(crate) fn enum_repr>() -> EnumRepr 75 | { 76 | let valid_reprs = ::iter() 77 | .map(Repr::from) 78 | .collect::>(); 79 | prop::sample::select(valid_reprs) 80 | } 81 | 82 | /// Strategy emitted by [`enum_repr()`] 83 | pub(crate) type EnumRepr = Select; 84 | 85 | /// [`BitmapIndex`] that's generated like a container size 86 | /// 87 | /// Many bitmap operations that take an index as a parameter have linear 88 | /// complexity as a function of said index, and bitmaps use linear storage 89 | /// proportional to their largest index, so it is not a good idea to use 90 | /// truly arbitrary indices in tests. Rather, we should bias towards smaller 91 | /// indices, like this strategy does. 92 | pub(crate) fn bitmap_index() -> BitmapIndexStrategy { 93 | let max_idx = SizeRange::default().end_excl(); 94 | (0..max_idx).prop_map(|idx| BitmapIndex::try_from(idx).unwrap_or(BitmapIndex::MAX)) 95 | } 96 | 97 | /// Strategy emitted by [`bitmap_index()`] 98 | pub(crate) type BitmapIndexStrategy = Map, fn(usize) -> BitmapIndex>; 99 | 100 | /// Pick a random object, mostly from [`Topology::test_instance()`] but 101 | /// sometimes from [`Topology::foreign_instance()`] as well 102 | #[cfg(test)] 103 | pub(crate) fn any_object() -> impl Strategy { 104 | #[cfg(feature = "hwloc-2_3_0")] 105 | { 106 | prop_oneof![ 107 | 4 => test_object(), 108 | 1 => prop::sample::select(Topology::foreign_objects()) 109 | ] 110 | } 111 | #[cfg(not(feature = "hwloc-2_3_0"))] 112 | test_object() 113 | } 114 | 115 | /// Pick a random object, from the test instance only 116 | /// 117 | /// Any method which takes an [`&TopologyObject`] as input should be robust to 118 | /// receiving inputs from foreign topologies, so unless there is a good reason 119 | /// to do otherwise you should prefer [`any_object()`] over [`test_object()`]. 120 | #[cfg(test)] 121 | pub(crate) fn test_object() -> impl Strategy { 122 | prop::sample::select(Topology::test_objects()) 123 | } 124 | 125 | /// [`CpuSet`] and [`NodeSet`] generator that is biased towards covering all 126 | /// set-theoretically interesting configurations with respect to a `reference` 127 | /// set that is somehow special to the function being tested: 128 | /// 129 | /// - Empty (nothing inside, nothing outside) 130 | /// - Disjoint (nothing inside, some outside) 131 | /// - Complement (nothing inside, everything outside) 132 | /// - Subset (some inside, nothing outside) 133 | /// - Intersects (some inside, some outside) 134 | /// - Subset complement (some inside, everything outside) 135 | /// - Equal (everything inside, nothing outside) 136 | /// - Superset (everything inside, some outside) 137 | /// - Everything (everything inside, everything outside) 138 | #[cfg(test)] 139 | pub(crate) fn set_with_reference( 140 | reference: SetRef, 141 | ) -> impl Strategy { 142 | // First, one of the reference set and its complement has to be finite 143 | let ref_set = reference.as_bitmap_ref(); 144 | let finite_set = if ref_set.weight().is_some() { 145 | ref_set.clone() 146 | } else { 147 | !ref_set 148 | }; 149 | assert!( 150 | finite_set.weight().is_some(), 151 | "since bitmaps can only be infinite in one direction, \ 152 | the complement of an infinite bitmap must be finite" 153 | ); 154 | 155 | // We can define a subset of the finite set that has a fair chance of 156 | // covering all finite set elements and none of them + other configurations 157 | let finite_elems = finite_set.iter_set().collect::>(); 158 | let num_finite_elems = finite_elems.len(); 159 | let inside_elems = prop_oneof![ 160 | 3 => prop::sample::subsequence(finite_elems.clone(), 0..=num_finite_elems), 161 | 1 => Just(Vec::new()), 162 | 1 => Just(finite_elems) 163 | ] 164 | .prop_map(|seq| seq.into_iter().collect::()); 165 | 166 | // Next we can pick a random set within the complement of the finite set by 167 | // picking a random set and subtracting the finite set from it 168 | let outside_elems = prop_oneof![ 169 | Just(Bitmap::new()), 170 | any::().prop_map(move |any_elems| any_elems - &finite_set), 171 | ]; 172 | 173 | // By combining these two sets which each have good coverage of all three 174 | // (nothing inside, some inside, everything inside) configurations of their 175 | // reference set, we get good coverage of all desired set configurations 176 | (inside_elems, outside_elems) 177 | .prop_map(|(inside_elems, outside_elems)| SetRef::Owned::from(inside_elems | outside_elems)) 178 | } 179 | 180 | /// Specialization of `set_with_reference` that uses the topology-wide cpuset or 181 | /// nodeset as a reference 182 | #[cfg(test)] 183 | pub(crate) fn topology_related_set( 184 | topology_set: impl FnOnce(&Topology) -> BitmapRef<'_, Set>, 185 | ) -> impl Strategy { 186 | set_with_reference(topology_set(Topology::test_instance())) 187 | } 188 | -------------------------------------------------------------------------------- /src/topology/export/mod.rs: -------------------------------------------------------------------------------- 1 | //! Exporting topologies to textual data 2 | //! 3 | //! An hwloc [`Topology`] can be saved to various textual formats. The resulting 4 | //! text files can then be re-imported later on, without going through the full 5 | //! hardware probing process again. 6 | //! 7 | //! Two formats are currently supported: 8 | //! 9 | //! - Synthetic topologies are a very simple textual representation that may only 10 | //! model certain topologies (they must be symmetric among other things, i.e. 11 | //! all CPU cores should be equal), and only some aspects of them (e.g. no I/O 12 | //! devices), but does so extremely concisely. 13 | //! - XML export can, in principle, handle every single topology that hwloc can 14 | //! probe, but does so at the cost of extra complexity. 15 | 16 | pub mod synthetic; 17 | pub mod xml; 18 | 19 | #[cfg(doc)] 20 | use super::Topology; 21 | -------------------------------------------------------------------------------- /src/topology/export/synthetic.rs: -------------------------------------------------------------------------------- 1 | //! Exporting topologies to Synthetic 2 | //! 3 | //! Synthetic topologies are a very simple textual representation that may only 4 | //! model certain topologies (they must be symmetric among other things, i.e. 5 | //! all CPU cores should be equal), and only some aspects of them (e.g. no I/O 6 | //! devices), but does so extremely concisely. 7 | 8 | #[cfg(doc)] 9 | use crate::topology::builder::TopologyBuilder; 10 | use crate::{ 11 | errors::{self, RawHwlocError}, 12 | ffi::int, 13 | topology::Topology, 14 | }; 15 | use bitflags::bitflags; 16 | use hwlocality_sys::{ 17 | hwloc_topology_export_synthetic_flags_e, HWLOC_TOPOLOGY_EXPORT_SYNTHETIC_FLAG_IGNORE_MEMORY, 18 | HWLOC_TOPOLOGY_EXPORT_SYNTHETIC_FLAG_NO_ATTRS, 19 | HWLOC_TOPOLOGY_EXPORT_SYNTHETIC_FLAG_NO_EXTENDED_TYPES, 20 | HWLOC_TOPOLOGY_EXPORT_SYNTHETIC_FLAG_V1, 21 | }; 22 | #[allow(unused)] 23 | #[cfg(test)] 24 | use similar_asserts::assert_eq; 25 | use std::ffi::{c_char, CString}; 26 | 27 | /// # Exporting Topologies to Synthetic 28 | // 29 | // --- Implementation details --- 30 | // 31 | // Upstream docs: https://hwloc.readthedocs.io/en/stable/group__hwlocality__syntheticexport.html 32 | impl Topology { 33 | /// Export the topology as a synthetic string 34 | /// 35 | /// This string may be loaded later using 36 | /// [`TopologyBuilder::from_synthetic()`]. 37 | /// 38 | /// I/O and Misc children are ignored, the synthetic string only describes 39 | /// normal children. 40 | /// 41 | /// By default, the exported topology is only meant to be compatible with 42 | /// the latest hwloc version. You may want to set some of the `flags` to be 43 | /// compatible with older hwloc releases, at the cost of dropping support 44 | /// for newer features. 45 | /// 46 | /// # Errors 47 | /// 48 | /// Synthetic topologies cannot express the full range of hardware 49 | /// topologies supported by hwloc, for example they don't support asymmetric 50 | /// topologies. An error will be returned if the current topology cannot be 51 | /// expressed as a synthetic topology. 52 | #[allow(clippy::missing_errors_doc, clippy::needless_continue)] 53 | #[doc(alias = "hwloc_topology_export_synthetic")] 54 | pub fn export_synthetic(&self, flags: SyntheticExportFlags) -> Result { 55 | let mut buf = vec![0u8; 1024]; // Size chosen per hwloc docs advice 56 | loop { 57 | let len = 58 | // SAFETY: - Topology is trusted to contain a valid ptr (type invariant) 59 | // - hwloc ops are trusted not to modify *const parameters 60 | // - buffer and buflen are in sync (same vector) 61 | // - flags only allows values supported by the active 62 | // hwloc version 63 | errors::call_hwloc_positive_or_minus1("hwloc_topology_export_synthetic", || unsafe { 64 | hwlocality_sys::hwloc_topology_export_synthetic( 65 | self.as_ptr(), 66 | buf.as_mut_ptr().cast::(), 67 | buf.len(), 68 | flags.bits(), 69 | ) 70 | })?; 71 | if int::expect_usize(len) == buf.len() - 1 { 72 | // hwloc exactly filled the buffer, which suggests the output 73 | // was truncated. Try a larger buffer. 74 | buf.resize(2 * buf.len(), 0); 75 | continue; 76 | } else { 77 | // Buffer seems alright, return it 78 | buf.truncate(len as usize + 1); 79 | return Ok(CString::from_vec_with_nul(buf) 80 | .expect("Missing NUL from hwloc") 81 | .into_string() 82 | .expect("Synthetic export should yield an ASCII string")); 83 | } 84 | } 85 | } 86 | } 87 | 88 | bitflags! { 89 | /// Flags to be given to [`Topology::export_synthetic()`] 90 | #[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq)] 91 | #[doc(alias = "hwloc_topology_export_synthetic_flags_e")] 92 | pub struct SyntheticExportFlags: hwloc_topology_export_synthetic_flags_e { 93 | /// Export extended types such as L2dcache as basic types such as Cache 94 | /// 95 | /// This is required if loading the synthetic description with hwloc 96 | /// < 1.9. 97 | #[doc(alias = "HWLOC_TOPOLOGY_EXPORT_SYNTHETIC_FLAG_NO_EXTENDED_TYPES")] 98 | const NO_EXTENDED_TYPES = HWLOC_TOPOLOGY_EXPORT_SYNTHETIC_FLAG_NO_EXTENDED_TYPES; 99 | 100 | /// Do not export level attributes 101 | /// 102 | /// Ignore level attributes such as memory/cache sizes or PU indices. 103 | /// 104 | /// This is required if loading the synthetic description with hwloc 105 | /// < 1.10. 106 | #[doc(alias = "HWLOC_TOPOLOGY_EXPORT_SYNTHETIC_FLAG_NO_ATTRS")] 107 | const NO_ATTRIBUTES = HWLOC_TOPOLOGY_EXPORT_SYNTHETIC_FLAG_NO_ATTRS; 108 | 109 | /// Export the memory hierarchy as expected in hwloc 1.x 110 | /// 111 | /// Instead of attaching memory children to levels, export single NUMA 112 | /// node children as normal intermediate levels, when possible. 113 | /// 114 | /// This is required if loading the synthetic description with hwloc 115 | /// 1.x. However this may fail if some objects have multiple local NUMA 116 | /// nodes. 117 | #[doc(alias = "HWLOC_TOPOLOGY_EXPORT_SYNTHETIC_FLAG_V1")] 118 | const V1 = HWLOC_TOPOLOGY_EXPORT_SYNTHETIC_FLAG_V1; 119 | 120 | /// Do not export memory information 121 | /// 122 | /// Only export the actual hierarchy of normal CPU-side objects and 123 | /// ignore where memory is attached. 124 | /// 125 | /// This is useful for when the hierarchy of CPUs is what really matters, 126 | /// but it behaves as if there was a single machine-wide NUMA node. 127 | #[doc(alias = "HWLOC_TOPOLOGY_EXPORT_SYNTHETIC_FLAG_IGNORE_MEMORY")] 128 | const IGNORE_MEMORY = HWLOC_TOPOLOGY_EXPORT_SYNTHETIC_FLAG_IGNORE_MEMORY; 129 | } 130 | } 131 | // 132 | crate::impl_arbitrary_for_bitflags!( 133 | SyntheticExportFlags, 134 | hwloc_topology_export_synthetic_flags_e 135 | ); 136 | 137 | #[cfg(test)] 138 | mod tests { 139 | use super::*; 140 | use crate::{ 141 | object::types::ObjectType, 142 | topology::{builder::TypeFilter, TopologyObject}, 143 | }; 144 | use proptest::prelude::*; 145 | #[allow(unused)] 146 | use similar_asserts::assert_eq; 147 | use strum::IntoEnumIterator; 148 | 149 | proptest! { 150 | #[test] 151 | fn export_synthetic(flags: SyntheticExportFlags) { 152 | let topology = Topology::test_instance(); 153 | let result = topology.export_synthetic(flags); 154 | 155 | // Per hwloc docs, exporting should always fail on asymmetric 156 | // topologies, and may fail for unspecified other reasons. 157 | if !topology.root_object().is_symmetric_subtree() { 158 | prop_assert!(result.is_err()); 159 | } 160 | let Ok(synthetic) = result else { return Ok(()); }; 161 | 162 | // Try to import back the topology 163 | let imported = Topology::builder().from_synthetic(&synthetic); 164 | if imported.is_err() { 165 | // Hwloc may not manage to load topologies exported with some 166 | // v1.x compatibility flags 167 | prop_assert!(flags.contains(SyntheticExportFlags::NO_EXTENDED_TYPES)); 168 | return Ok(()); 169 | } 170 | let imported = imported 171 | .expect("Synthetic strings from hwloc should be valid") 172 | .with_common_type_filter(TypeFilter::KeepAll) 173 | .unwrap() 174 | .build() 175 | .expect("Building a topology from an hwloc synthetic string should be valid"); 176 | 177 | // Check basic properties of the imported topology 178 | prop_assert_eq!(imported.cpuset().weight(), topology.cpuset().weight()); 179 | // 180 | if !flags.contains(SyntheticExportFlags::NO_EXTENDED_TYPES) { 181 | let flags_affecting_memobjs = SyntheticExportFlags::IGNORE_MEMORY | SyntheticExportFlags::V1; 182 | for ty in ObjectType::iter() { 183 | if ty.is_normal() 184 | || (ty.is_memory() && !flags.intersects(flags_affecting_memobjs)) 185 | { 186 | prop_assert_eq!( 187 | imported.objects_with_type(ty).count(), 188 | topology.objects_with_type(ty).count(), 189 | "count of objects of type {} doesn't match", ty 190 | ); 191 | } 192 | if ty.is_memory() && flags.contains(SyntheticExportFlags::IGNORE_MEMORY) { 193 | let expected_count = usize::from(ty == ObjectType::NUMANode); 194 | prop_assert_eq!( 195 | imported.objects_with_type(ty).count(), 196 | expected_count 197 | ); 198 | } 199 | } 200 | } 201 | // 202 | let total_memory = |topology: &Topology| topology.objects().map(TopologyObject::total_memory).sum::(); 203 | if !flags.intersects(SyntheticExportFlags::NO_ATTRIBUTES | SyntheticExportFlags::IGNORE_MEMORY) { 204 | prop_assert_eq!(total_memory(&imported), total_memory(topology)); 205 | }; 206 | } 207 | } 208 | } 209 | --------------------------------------------------------------------------------