├── .clang-format ├── .clang-tidy ├── .codespellrc ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── BUILDING.md ├── CMakeLists.txt ├── CMakePresets.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── HACKING.md ├── LICENSE ├── README.md ├── cmake ├── coverage.cmake ├── dev-mode.cmake ├── docs-ci.cmake ├── docs.cmake ├── folders.cmake ├── install-rules.cmake ├── install-script.cmake ├── lint-targets.cmake ├── lint.cmake ├── open-cpp-coverage.cmake.example ├── prelude.cmake ├── project-is-top-level.cmake ├── spell-targets.cmake ├── spell.cmake ├── variables.cmake └── windows-set-path.cmake ├── docs ├── Doxyfile.in ├── conf.py.in └── pages │ └── about.dox ├── images └── demo.png ├── source ├── main.cpp ├── searcher.cpp ├── searcher.hpp ├── sse2_strstr.cpp ├── sse2_strstr.hpp └── thread_pool.hpp └── test ├── CMakeLists.txt └── source └── oystr_test.cpp /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | # BasedOnStyle: Chromium 4 | AccessModifierOffset: -2 5 | AlignAfterOpenBracket: Align 6 | AlignConsecutiveMacros: false 7 | AlignConsecutiveAssignments: false 8 | AlignConsecutiveBitFields: false 9 | AlignConsecutiveDeclarations: false 10 | AlignEscapedNewlines: DontAlign 11 | AlignOperands: DontAlign 12 | AlignTrailingComments: false 13 | AllowAllArgumentsOnNextLine: true 14 | AllowAllConstructorInitializersOnNextLine: false 15 | AllowAllParametersOfDeclarationOnNextLine: true 16 | AllowShortEnumsOnASingleLine: false 17 | AllowShortBlocksOnASingleLine: Empty 18 | AllowShortCaseLabelsOnASingleLine: false 19 | AllowShortFunctionsOnASingleLine: Inline 20 | AllowShortLambdasOnASingleLine: All 21 | AllowShortIfStatementsOnASingleLine: Never 22 | AllowShortLoopsOnASingleLine: false 23 | AlwaysBreakAfterDefinitionReturnType: None 24 | AlwaysBreakAfterReturnType: None 25 | AlwaysBreakBeforeMultilineStrings: true 26 | AlwaysBreakTemplateDeclarations: Yes 27 | BinPackArguments: false 28 | BinPackParameters: false 29 | BraceWrapping: 30 | AfterCaseLabel: false 31 | AfterClass: true 32 | AfterControlStatement: MultiLine 33 | AfterEnum: true 34 | AfterFunction: true 35 | AfterNamespace: true 36 | AfterObjCDeclaration: false 37 | AfterStruct: true 38 | AfterUnion: true 39 | AfterExternBlock: true 40 | BeforeCatch: false 41 | BeforeElse: false 42 | BeforeLambdaBody: true 43 | BeforeWhile: false 44 | IndentBraces: false 45 | SplitEmptyFunction: true 46 | SplitEmptyRecord: true 47 | SplitEmptyNamespace: true 48 | BreakBeforeBinaryOperators: NonAssignment 49 | BreakBeforeBraces: Custom 50 | # BreakBeforeInheritanceComma: true 51 | BreakInheritanceList: BeforeComma 52 | BreakBeforeTernaryOperators: true 53 | BreakConstructorInitializersBeforeComma: true 54 | BreakConstructorInitializers: BeforeComma 55 | BreakAfterJavaFieldAnnotations: true 56 | BreakStringLiterals: true 57 | ColumnLimit: 80 58 | CommentPragmas: '^ IWYU pragma:' 59 | CompactNamespaces: false 60 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 61 | ConstructorInitializerIndentWidth: 4 62 | ContinuationIndentWidth: 4 63 | Cpp11BracedListStyle: true 64 | DeriveLineEnding: false 65 | DerivePointerAlignment: false 66 | DisableFormat: false 67 | ExperimentalAutoDetectBinPacking: false 68 | FixNamespaceComments: true 69 | ForEachMacros: 70 | - foreach 71 | - Q_FOREACH 72 | - BOOST_FOREACH 73 | IncludeBlocks: Regroup 74 | IncludeCategories: 75 | # Standard library headers come before anything else 76 | - Regex: '^<[a-z_]+>' 77 | Priority: -1 78 | - Regex: '^<.+\.h(pp)?>' 79 | Priority: 1 80 | - Regex: '^<.*' 81 | Priority: 2 82 | - Regex: '.*' 83 | Priority: 3 84 | IncludeIsMainRegex: '' 85 | IncludeIsMainSourceRegex: '' 86 | IndentCaseLabels: true 87 | IndentCaseBlocks: false 88 | IndentGotoLabels: true 89 | IndentPPDirectives: AfterHash 90 | IndentExternBlock: NoIndent 91 | IndentWidth: 2 92 | IndentWrappedFunctionNames: false 93 | InsertTrailingCommas: Wrapped 94 | JavaScriptQuotes: Double 95 | JavaScriptWrapImports: true 96 | KeepEmptyLinesAtTheStartOfBlocks: false 97 | MacroBlockBegin: '' 98 | MacroBlockEnd: '' 99 | MaxEmptyLinesToKeep: 1 100 | NamespaceIndentation: None 101 | ObjCBinPackProtocolList: Never 102 | ObjCBlockIndentWidth: 2 103 | ObjCBreakBeforeNestedBlockParam: true 104 | ObjCSpaceAfterProperty: false 105 | ObjCSpaceBeforeProtocolList: true 106 | PenaltyBreakAssignment: 2 107 | PenaltyBreakBeforeFirstCallParameter: 1 108 | PenaltyBreakComment: 300 109 | PenaltyBreakFirstLessLess: 120 110 | PenaltyBreakString: 1000 111 | PenaltyBreakTemplateDeclaration: 10 112 | PenaltyExcessCharacter: 1000000 113 | PenaltyReturnTypeOnItsOwnLine: 200 114 | PointerAlignment: Left 115 | RawStringFormats: 116 | - Language: Cpp 117 | Delimiters: 118 | - cc 119 | - CC 120 | - cpp 121 | - Cpp 122 | - CPP 123 | - 'c++' 124 | - 'C++' 125 | CanonicalDelimiter: '' 126 | BasedOnStyle: google 127 | - Language: TextProto 128 | Delimiters: 129 | - pb 130 | - PB 131 | - proto 132 | - PROTO 133 | EnclosingFunctions: 134 | - EqualsProto 135 | - EquivToProto 136 | - PARSE_PARTIAL_TEXT_PROTO 137 | - PARSE_TEST_PROTO 138 | - PARSE_TEXT_PROTO 139 | - ParseTextOrDie 140 | - ParseTextProtoOrDie 141 | - ParseTestProto 142 | - ParsePartialTestProto 143 | CanonicalDelimiter: '' 144 | BasedOnStyle: google 145 | ReflowComments: true 146 | SortIncludes: true 147 | SortUsingDeclarations: true 148 | SpaceAfterCStyleCast: false 149 | SpaceAfterLogicalNot: false 150 | SpaceAfterTemplateKeyword: false 151 | SpaceBeforeAssignmentOperators: true 152 | SpaceBeforeCpp11BracedList: true 153 | SpaceBeforeCtorInitializerColon: true 154 | SpaceBeforeInheritanceColon: true 155 | SpaceBeforeParens: ControlStatementsExceptForEachMacros 156 | SpaceBeforeRangeBasedForLoopColon: true 157 | SpaceInEmptyBlock: false 158 | SpaceInEmptyParentheses: false 159 | SpacesBeforeTrailingComments: 2 160 | SpacesInAngles: false 161 | SpacesInConditionalStatement: false 162 | SpacesInContainerLiterals: false 163 | SpacesInCStyleCastParentheses: false 164 | SpacesInParentheses: false 165 | SpacesInSquareBrackets: false 166 | SpaceBeforeSquareBrackets: false 167 | Standard: Auto 168 | StatementMacros: 169 | - Q_UNUSED 170 | - QT_REQUIRE_VERSION 171 | TabWidth: 8 172 | UseCRLF: false 173 | UseTab: Never 174 | WhitespaceSensitiveMacros: 175 | - STRINGIZE 176 | - PP_STRINGIZE 177 | - BOOST_PP_STRINGIZE 178 | ... 179 | -------------------------------------------------------------------------------- /.clang-tidy: -------------------------------------------------------------------------------- 1 | --- 2 | # Enable ALL the things! Except not really 3 | # misc-non-private-member-variables-in-classes: the options don't do anything 4 | Checks: "*,\ 5 | -google-readability-todo,\ 6 | -altera-*,\ 7 | -fuchsia-*,\ 8 | fuchsia-multiple-inheritance,\ 9 | -llvm-header-guard,\ 10 | -llvm-include-order,\ 11 | -llvmlibc-*,\ 12 | -misc-non-private-member-variables-in-classes" 13 | WarningsAsErrors: '' 14 | CheckOptions: 15 | - key: 'bugprone-argument-comment.StrictMode' 16 | value: 'true' 17 | # Prefer using enum classes with 2 values for parameters instead of bools 18 | - key: 'bugprone-argument-comment.CommentBoolLiterals' 19 | value: 'true' 20 | - key: 'bugprone-misplaced-widening-cast.CheckImplicitCasts' 21 | value: 'true' 22 | - key: 'bugprone-sizeof-expression.WarnOnSizeOfIntegerExpression' 23 | value: 'true' 24 | - key: 'bugprone-suspicious-string-compare.WarnOnLogicalNotComparison' 25 | value: 'true' 26 | - key: 'readability-simplify-boolean-expr.ChainedConditionalReturn' 27 | value: 'true' 28 | - key: 'readability-simplify-boolean-expr.ChainedConditionalAssignment' 29 | value: 'true' 30 | - key: 'readability-uniqueptr-delete-release.PreferResetCall' 31 | value: 'true' 32 | - key: 'cppcoreguidelines-init-variables.MathHeader' 33 | value: '' 34 | - key: 'cppcoreguidelines-narrowing-conversions.PedanticMode' 35 | value: 'true' 36 | - key: 'readability-else-after-return.WarnOnUnfixable' 37 | value: 'true' 38 | - key: 'readability-else-after-return.WarnOnConditionVariables' 39 | value: 'true' 40 | - key: 'readability-inconsistent-declaration-parameter-name.Strict' 41 | value: 'true' 42 | - key: 'readability-qualified-auto.AddConstToQualified' 43 | value: 'true' 44 | - key: 'readability-redundant-access-specifiers.CheckFirstDeclaration' 45 | value: 'true' 46 | # These seem to be the most common identifier styles 47 | - key: 'readability-identifier-naming.AbstractClassCase' 48 | value: 'lower_case' 49 | - key: 'readability-identifier-naming.ClassCase' 50 | value: 'lower_case' 51 | - key: 'readability-identifier-naming.ClassConstantCase' 52 | value: 'lower_case' 53 | - key: 'readability-identifier-naming.ClassMemberCase' 54 | value: 'lower_case' 55 | - key: 'readability-identifier-naming.ClassMethodCase' 56 | value: 'lower_case' 57 | - key: 'readability-identifier-naming.ConstantCase' 58 | value: 'lower_case' 59 | - key: 'readability-identifier-naming.ConstantMemberCase' 60 | value: 'lower_case' 61 | - key: 'readability-identifier-naming.ConstantParameterCase' 62 | value: 'lower_case' 63 | - key: 'readability-identifier-naming.ConstantPointerParameterCase' 64 | value: 'lower_case' 65 | - key: 'readability-identifier-naming.ConstexprFunctionCase' 66 | value: 'lower_case' 67 | - key: 'readability-identifier-naming.ConstexprMethodCase' 68 | value: 'lower_case' 69 | - key: 'readability-identifier-naming.ConstexprVariableCase' 70 | value: 'lower_case' 71 | - key: 'readability-identifier-naming.EnumCase' 72 | value: 'lower_case' 73 | - key: 'readability-identifier-naming.EnumConstantCase' 74 | value: 'lower_case' 75 | - key: 'readability-identifier-naming.FunctionCase' 76 | value: 'lower_case' 77 | - key: 'readability-identifier-naming.GlobalConstantCase' 78 | value: 'lower_case' 79 | - key: 'readability-identifier-naming.GlobalConstantPointerCase' 80 | value: 'lower_case' 81 | - key: 'readability-identifier-naming.GlobalFunctionCase' 82 | value: 'lower_case' 83 | - key: 'readability-identifier-naming.GlobalPointerCase' 84 | value: 'lower_case' 85 | - key: 'readability-identifier-naming.GlobalVariableCase' 86 | value: 'lower_case' 87 | - key: 'readability-identifier-naming.InlineNamespaceCase' 88 | value: 'lower_case' 89 | - key: 'readability-identifier-naming.LocalConstantCase' 90 | value: 'lower_case' 91 | - key: 'readability-identifier-naming.LocalConstantPointerCase' 92 | value: 'lower_case' 93 | - key: 'readability-identifier-naming.LocalPointerCase' 94 | value: 'lower_case' 95 | - key: 'readability-identifier-naming.LocalVariableCase' 96 | value: 'lower_case' 97 | - key: 'readability-identifier-naming.MacroDefinitionCase' 98 | value: 'UPPER_CASE' 99 | - key: 'readability-identifier-naming.MemberCase' 100 | value: 'lower_case' 101 | - key: 'readability-identifier-naming.MethodCase' 102 | value: 'lower_case' 103 | - key: 'readability-identifier-naming.NamespaceCase' 104 | value: 'lower_case' 105 | - key: 'readability-identifier-naming.ParameterCase' 106 | value: 'lower_case' 107 | - key: 'readability-identifier-naming.ParameterPackCase' 108 | value: 'lower_case' 109 | - key: 'readability-identifier-naming.PointerParameterCase' 110 | value: 'lower_case' 111 | - key: 'readability-identifier-naming.PrivateMemberCase' 112 | value: 'lower_case' 113 | - key: 'readability-identifier-naming.PrivateMemberPrefix' 114 | value: 'm_' 115 | - key: 'readability-identifier-naming.PrivateMethodCase' 116 | value: 'lower_case' 117 | - key: 'readability-identifier-naming.ProtectedMemberCase' 118 | value: 'lower_case' 119 | - key: 'readability-identifier-naming.ProtectedMemberPrefix' 120 | value: 'm_' 121 | - key: 'readability-identifier-naming.ProtectedMethodCase' 122 | value: 'lower_case' 123 | - key: 'readability-identifier-naming.PublicMemberCase' 124 | value: 'lower_case' 125 | - key: 'readability-identifier-naming.PublicMethodCase' 126 | value: 'lower_case' 127 | - key: 'readability-identifier-naming.ScopedEnumConstantCase' 128 | value: 'lower_case' 129 | - key: 'readability-identifier-naming.StaticConstantCase' 130 | value: 'lower_case' 131 | - key: 'readability-identifier-naming.StaticVariableCase' 132 | value: 'lower_case' 133 | - key: 'readability-identifier-naming.StructCase' 134 | value: 'lower_case' 135 | - key: 'readability-identifier-naming.TemplateParameterCase' 136 | value: 'CamelCase' 137 | - key: 'readability-identifier-naming.TemplateTemplateParameterCase' 138 | value: 'CamelCase' 139 | - key: 'readability-identifier-naming.TypeAliasCase' 140 | value: 'lower_case' 141 | - key: 'readability-identifier-naming.TypedefCase' 142 | value: 'lower_case' 143 | - key: 'readability-identifier-naming.TypeTemplateParameterCase' 144 | value: 'CamelCase' 145 | - key: 'readability-identifier-naming.UnionCase' 146 | value: 'lower_case' 147 | - key: 'readability-identifier-naming.ValueTemplateParameterCase' 148 | value: 'CamelCase' 149 | - key: 'readability-identifier-naming.VariableCase' 150 | value: 'lower_case' 151 | - key: 'readability-identifier-naming.VirtualMethodCase' 152 | value: 'lower_case' 153 | ... 154 | -------------------------------------------------------------------------------- /.codespellrc: -------------------------------------------------------------------------------- 1 | [codespell] 2 | builtin = clear,rare,en-GB_to_en-US,names,informal,code 3 | check-filenames = 4 | check-hidden = 5 | skip = */.git,*/build,*/prefix 6 | quiet-level = 2 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | pull_request: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - uses: actions/setup-python@v2 20 | with: { python-version: "3.8" } 21 | 22 | - name: Install codespell 23 | run: pip3 install codespell 24 | 25 | - name: Lint 26 | run: cmake -D FORMAT_COMMAND=clang-format-12 -P cmake/lint.cmake 27 | 28 | - name: Spell check 29 | if: always() 30 | run: cmake -P cmake/spell.cmake 31 | 32 | coverage: 33 | needs: [lint] 34 | 35 | runs-on: ubuntu-latest 36 | 37 | # To enable coverage, delete the last line from the conditional below and 38 | # edit the "" placeholder to your GitHub name. 39 | # If you do not wish to use codecov, then simply delete this job from the 40 | # workflow. 41 | if: github.repository_owner == '' 42 | && false 43 | 44 | steps: 45 | - uses: actions/checkout@v2 46 | 47 | - name: Install LCov 48 | run: sudo apt-get update -q 49 | && sudo apt-get install lcov -q -y 50 | 51 | - name: Configure 52 | run: cmake --preset=ci-coverage 53 | 54 | - name: Build 55 | run: cmake --build build/coverage -j 2 56 | 57 | - name: Test 58 | working-directory: build/coverage 59 | run: ctest --output-on-failure -j 2 60 | 61 | - name: Process coverage info 62 | run: cmake --build build/coverage -t coverage 63 | 64 | - name: Submit to codecov.io 65 | uses: codecov/codecov-action@v1 66 | with: 67 | file: build/coverage/coverage.info 68 | 69 | sanitize: 70 | needs: [lint] 71 | 72 | runs-on: ubuntu-latest 73 | 74 | steps: 75 | - uses: actions/checkout@v2 76 | 77 | - name: Configure 78 | env: { CXX: clang++-12 } 79 | run: cmake --preset=ci-sanitize 80 | 81 | - name: Build 82 | run: cmake --build build/sanitize -j 2 83 | 84 | - name: Test 85 | working-directory: build/sanitize 86 | env: 87 | ASAN_OPTIONS: "strict_string_checks=1:\ 88 | detect_stack_use_after_return=1:\ 89 | check_initialization_order=1:\ 90 | strict_init_order=1:\ 91 | detect_leaks=1" 92 | UBSAN_OPTIONS: print_stacktrace=1 93 | run: ctest --output-on-failure -j 2 94 | 95 | test: 96 | needs: [lint] 97 | 98 | strategy: 99 | matrix: 100 | os: [ubuntu-latest] 101 | 102 | runs-on: ${{ matrix.os }} 103 | 104 | steps: 105 | - uses: actions/checkout@v2 106 | 107 | - name: Install static analyzers 108 | if: matrix.os == 'ubuntu-latest' 109 | run: sudo apt-get install clang-tidy cppcheck -y -q 110 | 111 | - name: Configure 112 | shell: pwsh 113 | run: cmake "--preset=ci-$("${{ matrix.os }}".split("-")[0])" 114 | 115 | - name: Build 116 | run: cmake --build build --config Release -j 2 117 | 118 | - name: Install 119 | run: cmake --install build --config Release --prefix prefix 120 | 121 | - name: Test 122 | working-directory: build 123 | run: ctest --output-on-failure -C Release -j 2 124 | 125 | docs: 126 | # Deploy docs only when builds succeed 127 | needs: [sanitize, test] 128 | 129 | runs-on: ubuntu-latest 130 | 131 | # To enable, first you have to create an orphaned gh-pages branch: 132 | # 133 | # git switch --orphan gh-pages 134 | # git commit --allow-empty -m "Initial commit" 135 | # git push -u origin gh-pages 136 | # 137 | # Edit the placeholder below to your GitHub name, so this action 138 | # runs only in your repository and no one else's fork. After these, delete 139 | # this comment and the last line in the conditional below. 140 | # If you do not wish to use GitHub Pages for deploying documentation, then 141 | # simply delete this job similarly to the coverage one. 142 | if: github.ref == 'refs/heads/master' 143 | && github.event_name == 'push' 144 | && github.repository_owner == '' 145 | && false 146 | 147 | steps: 148 | - uses: actions/checkout@v2 149 | 150 | - uses: actions/setup-python@v2 151 | with: { python-version: "3.8" } 152 | 153 | - name: Install m.css dependencies 154 | run: pip3 install jinja2 Pygments 155 | 156 | - name: Install Doxygen 157 | run: sudo apt-get update -q 158 | && sudo apt-get install doxygen -q -y 159 | 160 | - name: Build docs 161 | run: cmake "-DPROJECT_SOURCE_DIR=$PWD" "-DPROJECT_BINARY_DIR=$PWD/build" 162 | -P cmake/docs-ci.cmake 163 | 164 | - name: Deploy docs 165 | uses: peaceiris/actions-gh-pages@v3 166 | with: 167 | github_token: ${{ secrets.GITHUB_TOKEN }} 168 | publish_dir: build/docs/html 169 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vs/ 3 | .vscode/ 4 | build/ 5 | cmake/open-cpp-coverage.cmake 6 | cmake-build-*/ 7 | prefix/ 8 | CMakeLists.txt.user 9 | CMakeUserPresets.json 10 | -------------------------------------------------------------------------------- /BUILDING.md: -------------------------------------------------------------------------------- 1 | # Building with CMake 2 | 3 | ## Build 4 | 5 | This project doesn't require any special command-line flags to build to keep 6 | things simple. 7 | 8 | Here are the steps for building in release mode with a single-configuration 9 | generator, like the Unix Makefiles one: 10 | 11 | ```sh 12 | cmake -S . -B build -D CMAKE_BUILD_TYPE=Release 13 | cmake --build build 14 | ``` 15 | 16 | Here are the steps for building in release mode with a multi-configuration 17 | generator, like the Visual Studio ones: 18 | 19 | ```sh 20 | cmake -S . -B build 21 | cmake --build build --config Release 22 | ``` 23 | 24 | ### Building with MSVC 25 | 26 | Note that MSVC by default is not standards compliant and you need to pass some 27 | flags to make it behave properly. See the `flags-windows` preset in the 28 | [CMakePresets.json](CMakePresets.json) file for the flags and with what 29 | variable to provide them to CMake during configuration. 30 | 31 | To build tests, include the option -D oystr_DEVELOPER_MODE=ON 32 | 33 | cmake -S . -B build -D oystr_DEVELOPER_MODE=ON 34 | cmake --build build 35 | 36 | ## Install 37 | 38 | This project doesn't require any special command-line flags to install to keep 39 | things simple. As a prerequisite, the project has to be built with the above 40 | commands already. 41 | 42 | The below commands require at least CMake 3.15 to run, because that is the 43 | version in which [Install a Project][1] was added. 44 | 45 | Here is the command for installing the release mode artifacts with a 46 | single-configuration generator, like the Unix Makefiles one: 47 | 48 | ```sh 49 | cmake --install build 50 | ``` 51 | 52 | Here is the command for installing the release mode artifacts with a 53 | multi-configuration generator, like the Visual Studio ones: 54 | 55 | ```sh 56 | cmake --install build --config Release 57 | ``` 58 | 59 | ### CMake package 60 | 61 | This project exports a CMake package to be used with the [`find_package`][2] 62 | command of CMake: 63 | 64 | * Package name: `oystr` 65 | * Cache variable: `OYSTR_EXECUTABLE` 66 | 67 | Example usage: 68 | 69 | ```cmake 70 | find_package(oystr REQUIRED) 71 | # Use the executable in some command 72 | execute_process( 73 | COMMAND "${OYSTR_EXECUTABLE}" ... 74 | ... 75 | ) 76 | ``` 77 | 78 | [1]: https://cmake.org/cmake/help/latest/manual/cmake.1.html#install-a-project 79 | [2]: https://cmake.org/cmake/help/latest/command/find_package.html 80 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | 3 | include(cmake/prelude.cmake) 4 | 5 | project( 6 | oystr 7 | VERSION 0.2.0 8 | DESCRIPTION "oystr recursively searches directories for a substring." 9 | HOMEPAGE_URL "https://github.com/p-ranav/oystr" 10 | LANGUAGES CXX 11 | ) 12 | 13 | include(cmake/project-is-top-level.cmake) 14 | include(cmake/variables.cmake) 15 | 16 | if(NOT CMAKE_BUILD_TYPE) 17 | set(CMAKE_BUILD_TYPE Release) 18 | endif() 19 | 20 | set(CMAKE_CXX_FLAGS_DEBUG "-g") 21 | set(CMAKE_CXX_FLAGS_RELEASE "-O3") 22 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=native") 23 | 24 | # ---- Argparse ------------- 25 | 26 | set(CMAKE_MODULE_PATH "") 27 | set(CMAKE_LIBRARY_ARCHITECTURE "") 28 | 29 | include(FetchContent) 30 | 31 | FetchContent_Declare(argparse 32 | GIT_REPOSITORY https://github.com/p-ranav/argparse.git 33 | GIT_TAG master 34 | ) 35 | FetchContent_MakeAvailable(argparse) 36 | 37 | # ---- Fmt ---------------- 38 | 39 | set(FMT_HEADERS "") 40 | 41 | FetchContent_Declare(fmt 42 | GIT_REPOSITORY https://github.com/fmtlib/fmt.git 43 | GIT_TAG master 44 | ) 45 | FetchContent_MakeAvailable(fmt) 46 | 47 | # ---- Threads ------------ 48 | find_package(Threads REQUIRED) 49 | 50 | # ---- Declare library ---- 51 | 52 | add_library( 53 | oystr_lib OBJECT 54 | source/searcher.cpp 55 | source/sse2_strstr.cpp 56 | ) 57 | 58 | target_include_directories( 59 | oystr_lib ${warning_guard} 60 | PUBLIC 61 | "$" 62 | ${argparse_SOURCE_DIR}/include/argparse 63 | ) 64 | 65 | target_compile_features(oystr_lib PUBLIC cxx_std_17) 66 | 67 | target_link_libraries(oystr_lib PRIVATE fmt::fmt Threads::Threads) 68 | 69 | # ---- Declare executable ---- 70 | 71 | add_executable(oystr_exe source/main.cpp) 72 | add_executable(oystr::exe ALIAS oystr_exe) 73 | 74 | set_target_properties( 75 | oystr_exe PROPERTIES 76 | OUTPUT_NAME oy 77 | EXPORT_NAME exe 78 | ) 79 | 80 | target_compile_features(oystr_exe PRIVATE cxx_std_17) 81 | 82 | target_link_libraries(oystr_exe PRIVATE oystr_lib fmt::fmt) 83 | 84 | # ---- Install rules ---- 85 | 86 | if(NOT CMAKE_SKIP_INSTALL_RULES) 87 | include(cmake/install-rules.cmake) 88 | endif() 89 | 90 | # ---- Developer mode ---- 91 | 92 | if(NOT oystr_DEVELOPER_MODE) 93 | return() 94 | elseif(NOT PROJECT_IS_TOP_LEVEL) 95 | message( 96 | AUTHOR_WARNING 97 | "Developer mode is intended for developers of oystr" 98 | ) 99 | endif() 100 | 101 | include(cmake/dev-mode.cmake) 102 | -------------------------------------------------------------------------------- /CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "cmakeMinimumRequired": { 4 | "major": 3, 5 | "minor": 14, 6 | "patch": 0 7 | }, 8 | "configurePresets": [ 9 | { 10 | "name": "cmake-pedantic", 11 | "hidden": true, 12 | "warnings": { 13 | "dev": true, 14 | "deprecated": true, 15 | "uninitialized": true, 16 | "unusedCli": true, 17 | "systemVars": false 18 | }, 19 | "errors": { 20 | "dev": true, 21 | "deprecated": true 22 | } 23 | }, 24 | { 25 | "name": "dev-mode", 26 | "hidden": true, 27 | "inherits": "cmake-pedantic", 28 | "cacheVariables": { 29 | "oystr_DEVELOPER_MODE": "ON" 30 | } 31 | }, 32 | { 33 | "name": "cppcheck", 34 | "hidden": true, 35 | "cacheVariables": { 36 | "CMAKE_CXX_CPPCHECK": "cppcheck;--inline-suppr" 37 | } 38 | }, 39 | { 40 | "name": "clang-tidy", 41 | "hidden": true, 42 | "cacheVariables": { 43 | "CMAKE_CXX_CLANG_TIDY": "clang-tidy;--header-filter=${sourceDir}/*" 44 | } 45 | }, 46 | { 47 | "name": "ci-std", 48 | "description": "This preset makes sure the project actually builds with at least the specified standard", 49 | "hidden": true, 50 | "cacheVariables": { 51 | "CMAKE_CXX_EXTENSIONS": "OFF", 52 | "CMAKE_CXX_STANDARD": "17", 53 | "CMAKE_CXX_STANDARD_REQUIRED": "ON" 54 | } 55 | }, 56 | { 57 | "name": "flags-unix", 58 | "hidden": true, 59 | "cacheVariables": { 60 | "CMAKE_CXX_FLAGS": "-Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion -Wcast-qual -Wshadow -Wformat=2 -Wundef -Werror=float-equal" 61 | } 62 | }, 63 | { 64 | "name": "flags-windows", 65 | "description": "Note that all the flags after /W4 are required for MSVC to conform to the language standard", 66 | "hidden": true, 67 | "cacheVariables": { 68 | "CMAKE_CXX_FLAGS": "/utf-8 /W4 /permissive- /volatile:iso /Zc:preprocessor /EHsc /Zc:__cplusplus /Zc:externConstexpr /Zc:throwingNew" 69 | } 70 | }, 71 | { 72 | "name": "ci-unix", 73 | "generator": "Unix Makefiles", 74 | "hidden": true, 75 | "inherits": ["flags-unix", "ci-std"], 76 | "cacheVariables": { 77 | "CMAKE_BUILD_TYPE": "Release" 78 | } 79 | }, 80 | { 81 | "name": "ci-win64", 82 | "inherits": ["flags-windows", "ci-std"], 83 | "generator": "Visual Studio 17 2022", 84 | "architecture": "x64", 85 | "hidden": true 86 | }, 87 | { 88 | "name": "coverage-unix", 89 | "binaryDir": "${sourceDir}/build/coverage", 90 | "inherits": "ci-unix", 91 | "hidden": true, 92 | "cacheVariables": { 93 | "ENABLE_COVERAGE": "ON", 94 | "CMAKE_BUILD_TYPE": "Coverage", 95 | "CMAKE_CXX_FLAGS_COVERAGE": "-Og -g --coverage -fkeep-inline-functions -fkeep-static-functions", 96 | "CMAKE_EXE_LINKER_FLAGS_COVERAGE": "--coverage", 97 | "CMAKE_SHARED_LINKER_FLAGS_COVERAGE": "--coverage" 98 | } 99 | }, 100 | { 101 | "name": "ci-coverage", 102 | "inherits": ["coverage-unix", "dev-mode"], 103 | "cacheVariables": { 104 | "COVERAGE_HTML_COMMAND": "" 105 | } 106 | }, 107 | { 108 | "name": "ci-sanitize", 109 | "binaryDir": "${sourceDir}/build/sanitize", 110 | "inherits": ["ci-unix", "dev-mode"], 111 | "cacheVariables": { 112 | "CMAKE_BUILD_TYPE": "Sanitize", 113 | "CMAKE_CXX_FLAGS_SANITIZE": "-O2 -g -fsanitize=address,undefined -fno-omit-frame-pointer -fno-common" 114 | } 115 | }, 116 | { 117 | "name": "ci-build", 118 | "binaryDir": "${sourceDir}/build", 119 | "hidden": true 120 | }, 121 | { 122 | "name": "ci-macos", 123 | "inherits": ["ci-build", "ci-unix", "dev-mode"] 124 | }, 125 | { 126 | "name": "ci-ubuntu", 127 | "inherits": ["ci-build", "ci-unix", "clang-tidy", "cppcheck", "dev-mode"] 128 | }, 129 | { 130 | "name": "ci-windows", 131 | "inherits": ["ci-build", "ci-win64", "dev-mode"] 132 | } 133 | ] 134 | } 135 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | * You will be judged by your contributions first, and your sense of humor 4 | second. 5 | * Nobody owes you anything. 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 7 | 8 | ## Code of Conduct 9 | 10 | Please see the [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) document. 11 | 12 | ## Getting started 13 | 14 | Helpful notes for developers can be found in the [`HACKING.md`](HACKING.md) 15 | document. 16 | 17 | In addition to he above, if you use the presets file as instructed, then you 18 | should NOT check it into source control, just as the CMake documentation 19 | suggests. 20 | -------------------------------------------------------------------------------- /HACKING.md: -------------------------------------------------------------------------------- 1 | # Hacking 2 | 3 | Here is some wisdom to help you build and test this project as a developer and 4 | potential contributor. 5 | 6 | If you plan to contribute, please read the [CONTRIBUTING](CONTRIBUTING.md) 7 | guide. 8 | 9 | ## Developer mode 10 | 11 | Build system targets that are only useful for developers of this project are 12 | hidden if the `oystr_DEVELOPER_MODE` option is disabled. Enabling this 13 | option makes tests and other developer targets and options available. Not 14 | enabling this option means that you are a consumer of this project and thus you 15 | have no need for these targets and options. 16 | 17 | Developer mode is always set to on in CI workflows. 18 | 19 | ### Presets 20 | 21 | This project makes use of [presets][1] to simplify the process of configuring 22 | the project. As a developer, you are recommended to always have the [latest 23 | CMake version][2] installed to make use of the latest Quality-of-Life 24 | additions. 25 | 26 | You have a few options to pass `oystr_DEVELOPER_MODE` to the configure 27 | command, but this project prefers to use presets. 28 | 29 | As a developer, you should create a `CMakeUserPresets.json` file at the root of 30 | the project: 31 | 32 | ```json 33 | { 34 | "version": 2, 35 | "cmakeMinimumRequired": { 36 | "major": 3, 37 | "minor": 14, 38 | "patch": 0 39 | }, 40 | "configurePresets": [ 41 | { 42 | "name": "dev", 43 | "binaryDir": "${sourceDir}/build/dev", 44 | "inherits": ["dev-mode", "ci-"], 45 | "cacheVariables": { 46 | "CMAKE_BUILD_TYPE": "Debug" 47 | } 48 | } 49 | ], 50 | "buildPresets": [ 51 | { 52 | "name": "dev", 53 | "configurePreset": "dev", 54 | "configuration": "Debug" 55 | } 56 | ], 57 | "testPresets": [ 58 | { 59 | "name": "dev", 60 | "configurePreset": "dev", 61 | "configuration": "Debug", 62 | "output": { 63 | "outputOnFailure": true 64 | } 65 | } 66 | ] 67 | } 68 | ``` 69 | 70 | You should replace `` in your newly created presets file with the name of 71 | the operating system you have, which may be `win64` or `unix`. You can see what 72 | these correspond to in the [`CMakePresets.json`](CMakePresets.json) file. 73 | 74 | `CMakeUserPresets.json` is also the perfect place in which you can put all 75 | sorts of things that you would otherwise want to pass to the configure command 76 | in the terminal. 77 | 78 | ### Configure, build and test 79 | 80 | If you followed the above instructions, then you can configure, build and test 81 | the project respectively with the following commands from the project root on 82 | any operating system with any build system: 83 | 84 | ```sh 85 | cmake --preset=dev 86 | cmake --build --preset=dev 87 | ctest --preset=dev 88 | ``` 89 | 90 | Please note that both the build and test command accepts a `-j` flag to specify 91 | the number of jobs to use, which should ideally be specified to the number of 92 | threads your CPU has. You may also want to add that to your preset using the 93 | `jobs` property, see the [presets documentation][1] for more details. 94 | 95 | [1]: https://cmake.org/cmake/help/latest/manual/cmake-presets.7.html 96 | [2]: https://cmake.org/download/ 97 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Pranav Srinivas Kumar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # oystr 2 | 3 | `oystr` is a command-line tool that recursively searches directories for a substring. 4 | 5 |

6 | 7 |

8 | 9 | ## Quick Start 10 | 11 | Build `oystr` using CMake. For more details, see [BUILDING.md](https://github.com/p-ranav/oystr/blob/master/BUILDING.md). 12 | 13 | ```bash 14 | git clone https://github.com/p-ranav/oystr 15 | cd oystr 16 | 17 | # Build 18 | cmake -S . -B build -D CMAKE_BUILD_TYPE=Release 19 | cmake --build build 20 | 21 | # Install 22 | sudo cmake --install build 23 | ``` 24 | -------------------------------------------------------------------------------- /cmake/coverage.cmake: -------------------------------------------------------------------------------- 1 | # ---- Variables ---- 2 | 3 | # We use variables separate from what CTest uses, because those have 4 | # customization issues 5 | set( 6 | COVERAGE_TRACE_COMMAND 7 | lcov -c -q 8 | -o "${PROJECT_BINARY_DIR}/coverage.info" 9 | -d "${PROJECT_BINARY_DIR}" 10 | --include "${PROJECT_SOURCE_DIR}/*" 11 | CACHE STRING 12 | "; separated command to generate a trace for the 'coverage' target" 13 | ) 14 | 15 | set( 16 | COVERAGE_HTML_COMMAND 17 | genhtml --legend -f -q 18 | "${PROJECT_BINARY_DIR}/coverage.info" 19 | -p "${PROJECT_SOURCE_DIR}" 20 | -o "${PROJECT_BINARY_DIR}/coverage_html" 21 | CACHE STRING 22 | "; separated command to generate an HTML report for the 'coverage' target" 23 | ) 24 | 25 | # ---- Coverage target ---- 26 | 27 | add_custom_target( 28 | coverage 29 | COMMAND ${COVERAGE_TRACE_COMMAND} 30 | COMMAND ${COVERAGE_HTML_COMMAND} 31 | COMMENT "Generating coverage report" 32 | VERBATIM 33 | ) 34 | -------------------------------------------------------------------------------- /cmake/dev-mode.cmake: -------------------------------------------------------------------------------- 1 | include(cmake/folders.cmake) 2 | 3 | include(CTest) 4 | if(BUILD_TESTING) 5 | add_subdirectory(test) 6 | endif() 7 | 8 | add_custom_target( 9 | run-exe 10 | COMMAND oystr_exe 11 | VERBATIM 12 | ) 13 | add_dependencies(run-exe oystr_exe) 14 | 15 | option(BUILD_MCSS_DOCS "Build documentation using Doxygen and m.css" OFF) 16 | if(BUILD_MCSS_DOCS) 17 | include(cmake/docs.cmake) 18 | endif() 19 | 20 | option(ENABLE_COVERAGE "Enable coverage support separate from CTest's" OFF) 21 | if(ENABLE_COVERAGE) 22 | include(cmake/coverage.cmake) 23 | endif() 24 | 25 | if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows") 26 | include(cmake/open-cpp-coverage.cmake OPTIONAL) 27 | endif() 28 | 29 | include(cmake/lint-targets.cmake) 30 | include(cmake/spell-targets.cmake) 31 | 32 | add_folders(Project) 33 | -------------------------------------------------------------------------------- /cmake/docs-ci.cmake: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | 3 | foreach(var IN ITEMS PROJECT_BINARY_DIR PROJECT_SOURCE_DIR) 4 | if(NOT DEFINED "${var}") 5 | message(FATAL_ERROR "${var} must be defined") 6 | endif() 7 | endforeach() 8 | set(bin "${PROJECT_BINARY_DIR}") 9 | set(src "${PROJECT_SOURCE_DIR}") 10 | 11 | # ---- Dependencies ---- 12 | 13 | set(mcss_SOURCE_DIR "${bin}/docs/.ci") 14 | if(NOT IS_DIRECTORY "${mcss_SOURCE_DIR}") 15 | file(MAKE_DIRECTORY "${mcss_SOURCE_DIR}") 16 | file( 17 | DOWNLOAD 18 | https://github.com/friendlyanon/m.css/releases/download/release-1/mcss.zip 19 | "${mcss_SOURCE_DIR}/mcss.zip" 20 | STATUS status 21 | EXPECTED_MD5 00cd2757ebafb9bcba7f5d399b3bec7f 22 | ) 23 | if(NOT status MATCHES "^0;") 24 | message(FATAL_ERROR "Download failed with ${status}") 25 | endif() 26 | execute_process( 27 | COMMAND "${CMAKE_COMMAND}" -E tar xf mcss.zip 28 | WORKING_DIRECTORY "${mcss_SOURCE_DIR}" 29 | RESULT_VARIABLE result 30 | ) 31 | if(NOT result EQUAL "0") 32 | message(FATAL_ERROR "Extraction failed with ${result}") 33 | endif() 34 | file(REMOVE "${mcss_SOURCE_DIR}/mcss.zip") 35 | endif() 36 | 37 | find_program(Python3_EXECUTABLE NAMES python3 python) 38 | if(NOT Python3_EXECUTABLE) 39 | message(FATAL_ERROR "Python executable was not found") 40 | endif() 41 | 42 | # ---- Process project() call in CMakeLists.txt ---- 43 | 44 | file(READ "${src}/CMakeLists.txt" content) 45 | 46 | string(FIND "${content}" "project(" index) 47 | if(index EQUAL "-1") 48 | message(FATAL_ERROR "Could not find \"project(\"") 49 | endif() 50 | string(SUBSTRING "${content}" "${index}" -1 content) 51 | 52 | string(FIND "${content}" "\n)\n" index) 53 | if(index EQUAL "-1") 54 | message(FATAL_ERROR "Could not find \"\\n)\\n\"") 55 | endif() 56 | string(SUBSTRING "${content}" 0 "${index}" content) 57 | 58 | file(WRITE "${bin}/docs-ci.project.cmake" "docs_${content}\n)\n") 59 | 60 | macro(list_pop_front list out) 61 | list(GET "${list}" 0 "${out}") 62 | list(REMOVE_AT "${list}" 0) 63 | endmacro() 64 | 65 | function(docs_project name) 66 | cmake_parse_arguments(PARSE_ARGV 1 "" "" "VERSION;DESCRIPTION;HOMEPAGE_URL" LANGUAGES) 67 | set(PROJECT_NAME "${name}" PARENT_SCOPE) 68 | if(DEFINED _VERSION) 69 | set(PROJECT_VERSION "${_VERSION}" PARENT_SCOPE) 70 | string(REGEX MATCH "^[0-9]+(\\.[0-9]+)*" versions "${_VERSION}") 71 | string(REPLACE . ";" versions "${versions}") 72 | set(suffixes MAJOR MINOR PATCH TWEAK) 73 | while(NOT versions STREQUAL "" AND NOT suffixes STREQUAL "") 74 | list_pop_front(versions version) 75 | list_pop_front(suffixes suffix) 76 | set("PROJECT_VERSION_${suffix}" "${version}" PARENT_SCOPE) 77 | endwhile() 78 | endif() 79 | if(DEFINED _DESCRIPTION) 80 | set(PROJECT_DESCRIPTION "${_DESCRIPTION}" PARENT_SCOPE) 81 | endif() 82 | if(DEFINED _HOMEPAGE_URL) 83 | set(PROJECT_HOMEPAGE_URL "${_HOMEPAGE_URL}" PARENT_SCOPE) 84 | endif() 85 | endfunction() 86 | 87 | include("${bin}/docs-ci.project.cmake") 88 | 89 | # ---- Generate docs ---- 90 | 91 | if(NOT DEFINED DOXYGEN_OUTPUT_DIRECTORY) 92 | set(DOXYGEN_OUTPUT_DIRECTORY "${bin}/docs") 93 | endif() 94 | set(out "${DOXYGEN_OUTPUT_DIRECTORY}") 95 | 96 | foreach(file IN ITEMS Doxyfile conf.py) 97 | configure_file("${src}/docs/${file}.in" "${bin}/docs/${file}" @ONLY) 98 | endforeach() 99 | 100 | set(mcss_script "${mcss_SOURCE_DIR}/documentation/doxygen.py") 101 | set(config "${bin}/docs/conf.py") 102 | 103 | file(REMOVE_RECURSE "${out}/html" "${out}/xml") 104 | 105 | execute_process( 106 | COMMAND "${Python3_EXECUTABLE}" "${mcss_script}" "${config}" 107 | WORKING_DIRECTORY "${bin}/docs" 108 | RESULT_VARIABLE result 109 | ) 110 | if(NOT result EQUAL "0") 111 | message(FATAL_ERROR "m.css returned with ${result}") 112 | endif() 113 | -------------------------------------------------------------------------------- /cmake/docs.cmake: -------------------------------------------------------------------------------- 1 | # ---- Dependencies ---- 2 | 3 | include(FetchContent) 4 | FetchContent_Declare( 5 | mcss URL 6 | https://github.com/friendlyanon/m.css/releases/download/release-1/mcss.zip 7 | URL_MD5 00cd2757ebafb9bcba7f5d399b3bec7f 8 | SOURCE_DIR "${PROJECT_BINARY_DIR}/mcss" 9 | UPDATE_DISCONNECTED YES 10 | ) 11 | FetchContent_MakeAvailable(mcss) 12 | 13 | find_package(Python3 3.6 REQUIRED) 14 | 15 | # ---- Declare documentation target ---- 16 | 17 | set( 18 | DOXYGEN_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/docs" 19 | CACHE PATH "Path for the generated Doxygen documentation" 20 | ) 21 | 22 | set(working_dir "${PROJECT_BINARY_DIR}/docs") 23 | 24 | foreach(file IN ITEMS Doxyfile conf.py) 25 | configure_file("docs/${file}.in" "${working_dir}/${file}" @ONLY) 26 | endforeach() 27 | 28 | set(mcss_script "${mcss_SOURCE_DIR}/documentation/doxygen.py") 29 | set(config "${working_dir}/conf.py") 30 | 31 | add_custom_target( 32 | docs 33 | COMMAND "${CMAKE_COMMAND}" -E remove_directory 34 | "${DOXYGEN_OUTPUT_DIRECTORY}/html" 35 | "${DOXYGEN_OUTPUT_DIRECTORY}/xml" 36 | COMMAND "${Python3_EXECUTABLE}" "${mcss_script}" "${config}" 37 | COMMENT "Building documentation using Doxygen and m.css" 38 | WORKING_DIRECTORY "${working_dir}" 39 | VERBATIM 40 | ) 41 | -------------------------------------------------------------------------------- /cmake/folders.cmake: -------------------------------------------------------------------------------- 1 | set_property(GLOBAL PROPERTY USE_FOLDERS YES) 2 | 3 | # Call this function at the end of a directory scope to assign a folder to 4 | # targets created in that directory. Utility targets will be assigned to the 5 | # UtilityTargets folder, otherwise to the ${name}Targets folder. If a target 6 | # already has a folder assigned, then that target will be skipped. 7 | function(add_folders name) 8 | get_property(targets DIRECTORY PROPERTY BUILDSYSTEM_TARGETS) 9 | foreach(target IN LISTS targets) 10 | get_property(folder TARGET "${target}" PROPERTY FOLDER) 11 | if(DEFINED folder) 12 | continue() 13 | endif() 14 | set(folder Utility) 15 | get_property(type TARGET "${target}" PROPERTY TYPE) 16 | if(NOT type STREQUAL "UTILITY") 17 | set(folder "${name}") 18 | endif() 19 | set_property(TARGET "${target}" PROPERTY FOLDER "${folder}Targets") 20 | endforeach() 21 | endfunction() 22 | -------------------------------------------------------------------------------- /cmake/install-rules.cmake: -------------------------------------------------------------------------------- 1 | include(CMakePackageConfigHelpers) 2 | include(GNUInstallDirs) 3 | 4 | # find_package() call for consumers to find this project 5 | set(package oystr) 6 | 7 | install( 8 | TARGETS oystr_exe 9 | RUNTIME COMPONENT oystr_Runtime 10 | ) 11 | 12 | write_basic_package_version_file( 13 | "${package}ConfigVersion.cmake" 14 | COMPATIBILITY SameMajorVersion 15 | ) 16 | 17 | # Allow package maintainers to freely override the path for the configs 18 | set( 19 | oystr_INSTALL_CMAKEDIR "${CMAKE_INSTALL_DATADIR}/${package}" 20 | CACHE PATH "CMake package config location relative to the install prefix" 21 | ) 22 | mark_as_advanced(oystr_INSTALL_CMAKEDIR) 23 | 24 | install( 25 | FILES "${PROJECT_BINARY_DIR}/${package}ConfigVersion.cmake" 26 | DESTINATION "${oystr_INSTALL_CMAKEDIR}" 27 | COMPONENT oystr_Development 28 | ) 29 | 30 | # Export variables for the install script to use 31 | install(CODE " 32 | set(oystr_NAME [[$]]) 33 | set(oystr_INSTALL_CMAKEDIR [[${oystr_INSTALL_CMAKEDIR}]]) 34 | set(CMAKE_INSTALL_BINDIR [[${CMAKE_INSTALL_BINDIR}]]) 35 | " COMPONENT oystr_Development) 36 | 37 | install( 38 | SCRIPT cmake/install-script.cmake 39 | COMPONENT oystr_Development 40 | ) 41 | 42 | if(PROJECT_IS_TOP_LEVEL) 43 | include(CPack) 44 | endif() 45 | -------------------------------------------------------------------------------- /cmake/install-script.cmake: -------------------------------------------------------------------------------- 1 | file( 2 | RELATIVE_PATH relative_path 3 | "/${oystr_INSTALL_CMAKEDIR}" 4 | "/${CMAKE_INSTALL_BINDIR}/${oystr_NAME}" 5 | ) 6 | 7 | get_filename_component(prefix "${CMAKE_INSTALL_PREFIX}" ABSOLUTE) 8 | set(config_dir "${prefix}/${oystr_INSTALL_CMAKEDIR}") 9 | set(config_file "${config_dir}/oystrConfig.cmake") 10 | 11 | message(STATUS "Installing: ${config_file}") 12 | file(WRITE "${config_file}" "\ 13 | get_filename_component( 14 | _oystr_executable 15 | \"\${CMAKE_CURRENT_LIST_DIR}/${relative_path}\" 16 | ABSOLUTE 17 | ) 18 | set( 19 | OYSTR_EXECUTABLE \"\${_oystr_executable}\" 20 | CACHE FILEPATH \"Path to the oystr executable\" 21 | ) 22 | ") 23 | list(APPEND CMAKE_INSTALL_MANIFEST_FILES "${config_file}") 24 | -------------------------------------------------------------------------------- /cmake/lint-targets.cmake: -------------------------------------------------------------------------------- 1 | set( 2 | FORMAT_PATTERNS 3 | source/*.cpp source/*.hpp 4 | include/*.hpp 5 | test/*.cpp test/*.hpp 6 | CACHE STRING 7 | "; separated patterns relative to the project source dir to format" 8 | ) 9 | 10 | set(FORMAT_COMMAND clang-format CACHE STRING "Formatter to use") 11 | 12 | add_custom_target( 13 | format-check 14 | COMMAND "${CMAKE_COMMAND}" 15 | -D "FORMAT_COMMAND=${FORMAT_COMMAND}" 16 | -D "PATTERNS=${FORMAT_PATTERNS}" 17 | -P "${PROJECT_SOURCE_DIR}/cmake/lint.cmake" 18 | WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" 19 | COMMENT "Linting the code" 20 | VERBATIM 21 | ) 22 | 23 | add_custom_target( 24 | format-fix 25 | COMMAND "${CMAKE_COMMAND}" 26 | -D "FORMAT_COMMAND=${FORMAT_COMMAND}" 27 | -D "PATTERNS=${FORMAT_PATTERNS}" 28 | -D FIX=YES 29 | -P "${PROJECT_SOURCE_DIR}/cmake/lint.cmake" 30 | WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" 31 | COMMENT "Fixing the code" 32 | VERBATIM 33 | ) 34 | -------------------------------------------------------------------------------- /cmake/lint.cmake: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | 3 | macro(default name) 4 | if(NOT DEFINED "${name}") 5 | set("${name}" "${ARGN}") 6 | endif() 7 | endmacro() 8 | 9 | default(FORMAT_COMMAND clang-format) 10 | default( 11 | PATTERNS 12 | source/*.cpp source/*.hpp 13 | include/*.hpp 14 | test/*.cpp test/*.hpp 15 | ) 16 | default(FIX NO) 17 | 18 | set(flag --output-replacements-xml) 19 | set(args OUTPUT_VARIABLE output) 20 | if(FIX) 21 | set(flag -i) 22 | set(args "") 23 | endif() 24 | 25 | file(GLOB_RECURSE files ${PATTERNS}) 26 | set(badly_formatted "") 27 | set(output "") 28 | string(LENGTH "${CMAKE_SOURCE_DIR}/" path_prefix_length) 29 | 30 | foreach(file IN LISTS files) 31 | execute_process( 32 | COMMAND "${FORMAT_COMMAND}" --style=file "${flag}" "${file}" 33 | WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" 34 | RESULT_VARIABLE result 35 | ${args} 36 | ) 37 | if(NOT result EQUAL "0") 38 | message(FATAL_ERROR "'${file}': formatter returned with ${result}") 39 | endif() 40 | if(NOT FIX AND output MATCHES "\n ...) 5 | function(windows_set_path TEST) 6 | if(NOT CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows") 7 | return() 8 | endif() 9 | 10 | set(path "") 11 | set(glue "") 12 | foreach(target IN LISTS ARGN) 13 | if(TARGET "${target}") 14 | get_target_property(type "${target}" TYPE) 15 | if(type STREQUAL "SHARED_LIBRARY") 16 | set(path "${path}${glue}$") 17 | set(glue "\;") # backslash is important 18 | endif() 19 | endif() 20 | endforeach() 21 | if(NOT path STREQUAL "") 22 | set_property(TEST "${TEST}" PROPERTY ENVIRONMENT "PATH=${path}") 23 | endif() 24 | endfunction() 25 | -------------------------------------------------------------------------------- /docs/Doxyfile.in: -------------------------------------------------------------------------------- 1 | # Configuration for Doxygen for use with CMake 2 | # Only options that deviate from the default are included 3 | # To create a new Doxyfile containing all available options, call `doxygen -g` 4 | 5 | # Get Project name and version from CMake 6 | PROJECT_NAME = "@PROJECT_NAME@" 7 | PROJECT_NUMBER = "@PROJECT_VERSION@" 8 | 9 | # Add sources 10 | INPUT = "@PROJECT_SOURCE_DIR@/README.md" "@PROJECT_SOURCE_DIR@/include" "@PROJECT_SOURCE_DIR@/source" "@PROJECT_SOURCE_DIR@/docs/pages" 11 | EXTRACT_ALL = YES 12 | RECURSIVE = YES 13 | OUTPUT_DIRECTORY = "@DOXYGEN_OUTPUT_DIRECTORY@" 14 | 15 | # Use the README as a main page 16 | USE_MDFILE_AS_MAINPAGE = "@PROJECT_SOURCE_DIR@/README.md" 17 | 18 | # set relative include paths 19 | FULL_PATH_NAMES = YES 20 | STRIP_FROM_PATH = "@PROJECT_SOURCE_DIR@/include" "@PROJECT_SOURCE_DIR@" 21 | STRIP_FROM_INC_PATH = 22 | 23 | # We use m.css to generate the html documentation, so we only need XML output 24 | GENERATE_XML = YES 25 | GENERATE_HTML = NO 26 | GENERATE_LATEX = NO 27 | XML_PROGRAMLISTING = NO 28 | CREATE_SUBDIRS = NO 29 | 30 | # Include all directories, files and namespaces in the documentation 31 | # Disable to include only explicitly documented objects 32 | M_SHOW_UNDOCUMENTED = YES 33 | -------------------------------------------------------------------------------- /docs/conf.py.in: -------------------------------------------------------------------------------- 1 | DOXYFILE = 'Doxyfile' 2 | 3 | LINKS_NAVBAR1 = [ 4 | (None, 'pages', [(None, 'about')]), 5 | (None, 'namespaces', []), 6 | ] 7 | -------------------------------------------------------------------------------- /docs/pages/about.dox: -------------------------------------------------------------------------------- 1 | /** 2 | * @page about About 3 | * @section about-doxygen Doxygen documentation 4 | * This page is auto generated using 5 | * Doxygen, making use of some useful 6 | * special commands. 7 | */ 8 | -------------------------------------------------------------------------------- /images/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p-ranav/oystr/a4347812e0cfeda9a197bd0956b7f9bf291fbaa7/images/demo.png -------------------------------------------------------------------------------- /source/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | namespace fs = std::filesystem; 5 | 6 | int main(int argc, char* argv[]) 7 | { 8 | const auto is_path_from_terminal = isatty(STDIN_FILENO) == 1; 9 | const auto is_stdout = isatty(STDOUT_FILENO) == 1; 10 | std::ios_base::sync_with_stdio(false); 11 | std::cin.tie(NULL); 12 | argparse::ArgumentParser program("search", "0.2.0\n"); 13 | program.add_argument("query"); 14 | program.add_argument("path").remaining(); 15 | 16 | // Generic Program Information 17 | program.add_argument("-h", "--help") 18 | .help("Shows help message and exits") 19 | .default_value(false) 20 | .implicit_value(true); 21 | 22 | program.add_argument("-f", "--filter") 23 | .help("Only evaluate files that match filter pattern") 24 | .default_value(std::string {"*.*"}); 25 | 26 | program.add_argument("-j") 27 | .help("Number of threads") 28 | .scan<'d', int>() 29 | .default_value(5); 30 | 31 | try { 32 | program.parse_args(argc, argv); 33 | } catch (const std::runtime_error& err) { 34 | std::cerr << err.what() << std::endl; 35 | std::cerr << program; 36 | std::exit(1); 37 | } 38 | 39 | enum class file_option_t 40 | { 41 | none, 42 | single_file, 43 | single_directory, 44 | multiple 45 | }; 46 | 47 | file_option_t file_option; 48 | 49 | std::vector paths; 50 | if (is_path_from_terminal) { 51 | // Input arguments ARE paths to files or directories 52 | // Parse the arguments 53 | try { 54 | paths = program.get>("path"); 55 | auto size = paths.size(); 56 | 57 | if (size == 1) { 58 | if (fs::is_regular_file(fs::path(paths[0]))) { 59 | file_option = file_option_t::single_file; 60 | } else if (fs::is_directory(fs::path(paths[0]))) { 61 | file_option = file_option_t::single_directory; 62 | } 63 | } else { 64 | file_option = file_option_t::multiple; 65 | } 66 | } catch (std::logic_error& e) { 67 | // No files provided 68 | file_option = file_option_t::none; 69 | } 70 | } 71 | 72 | auto query = program.get("query"); 73 | auto filter = program.get("-f"); 74 | auto num_threads = program.get("-j"); 75 | 76 | // Configure a searcher 77 | search::searcher searcher; 78 | searcher.m_query = query; 79 | searcher.m_filter = filter; 80 | searcher.m_is_stdout = is_stdout; 81 | searcher.m_is_path_from_terminal = is_path_from_terminal; 82 | 83 | if (is_path_from_terminal) { 84 | searcher.m_ts = std::make_unique(num_threads); 85 | // Input arguments ARE paths to files or directories 86 | if (file_option == file_option_t::none) { 87 | searcher.directory_search("."); 88 | } else if (file_option == file_option_t::single_file) { 89 | searcher.read_file_and_search((const char*)paths[0].c_str()); 90 | } else if (file_option == file_option_t::single_directory) { 91 | searcher.directory_search((const char*)paths[0].c_str()); 92 | } else if (file_option == file_option_t::multiple) { 93 | for (const auto& path : paths) { 94 | if (fs::is_regular_file(fs::path(path))) { 95 | searcher.read_file_and_search((const char*)path.c_str()); 96 | } else if (fs::is_directory(fs::path(path))) { 97 | searcher.directory_search((const char*)path.c_str()); 98 | } else { 99 | fmt::print(fmt::fg(fmt::color::red) | fmt::emphasis::bold, 100 | "\nError: '{}' is not a valid file or directory\n", 101 | path); 102 | std::exit(1); 103 | } 104 | } 105 | } 106 | } else { 107 | // Input is from pipe 108 | for (std::string line; std::getline(std::cin, line);) { 109 | if (!line.empty() && line.size() >= query.size()) { 110 | searcher.file_search("", line); 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /source/searcher.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | namespace fs = std::filesystem; 4 | 5 | /* We want POSIX.1-2008 + XSI, i.e. SuSv4, features */ 6 | #ifndef _XOPEN_SOURCE 7 | # define _XOPEN_SOURCE 700 8 | #endif 9 | 10 | /* If the C library can support 64-bit file sizes 11 | and offsets, using the standard names, 12 | these defines tell the C library to do so. */ 13 | #ifndef _LARGEFILE64_SOURCE 14 | # define _LARGEFILE64_SOURCE 15 | #endif 16 | 17 | #ifndef _FILE_OFFSET_BITS 18 | # define _FILE_OFFSET_BITS 64 19 | #endif 20 | 21 | #include 22 | 23 | #ifndef _GNU_SOURCE 24 | # define _GNU_SOURCE 25 | #endif 26 | 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | 33 | /* POSIX.1 says each process has at least 20 file descriptors. 34 | * Three of those belong to the standard streams. 35 | * Here, we use a conservative estimate of 15 available; 36 | * assuming we use at most two for other uses in this program, 37 | * we should never run into any problems. 38 | * Most trees are shallower than that, so it is efficient. 39 | * Deeper trees are traversed fine, just a bit slower. 40 | * (Linux allows typically hundreds to thousands of open files, 41 | * so you'll probably never see any issues even if you used 42 | * a much higher value, say a couple of hundred, but 43 | * 15 is a safe, reasonable value.) 44 | */ 45 | #ifndef USE_FDS 46 | # define USE_FDS 15 47 | #endif 48 | 49 | namespace search 50 | { 51 | std::string_view::const_iterator needle_search( 52 | std::string_view needle, 53 | std::string_view::const_iterator haystack_begin, 54 | std::string_view::const_iterator haystack_end) 55 | { 56 | if (haystack_begin != haystack_end) { 57 | return std::search( 58 | haystack_begin, haystack_end, needle.begin(), needle.end()); 59 | } else { 60 | return haystack_end; 61 | } 62 | } 63 | 64 | auto find_needle_position(std::string_view str, std::string_view query) 65 | { 66 | auto it = needle_search(query, str.begin(), str.end()); 67 | 68 | return it != str.end() ? std::size_t(it - str.begin()) 69 | : std::string_view::npos; 70 | } 71 | 72 | void print_colored(std::string_view str, 73 | std::string_view query, 74 | fmt::memory_buffer& out) 75 | { 76 | auto pos = find_needle_position(str, query); 77 | if (pos == std::string_view::npos) { 78 | fmt::format_to(std::back_inserter(out), "{}\n", str); 79 | return; 80 | } 81 | fmt::format_to(std::back_inserter(out), "{}", str.substr(0, pos)); 82 | fmt::format_to(std::back_inserter(out), 83 | "\033[1;31m{}\033[0m", 84 | str.substr(pos, query.size())); 85 | print_colored(str.substr(pos + query.size()), query, out); 86 | } 87 | 88 | std::size_t searcher::file_search(std::string_view filename, 89 | std::string_view haystack) 90 | 91 | { 92 | auto out = fmt::memory_buffer(); 93 | 94 | // Start from the beginning 95 | const auto haystack_begin = haystack.cbegin(); 96 | const auto haystack_end = haystack.cend(); 97 | 98 | auto it = haystack_begin; 99 | bool first_search = true; 100 | bool printed_file_name = false; 101 | std::size_t current_line_number = 1; 102 | auto no_file_name = filename.empty(); 103 | 104 | while (it != haystack_end) { 105 | #if defined(__SSE2__) 106 | std::string_view view(it, haystack_end - it); 107 | if (view.empty()) { 108 | it = haystack_end; 109 | break; 110 | } else { 111 | auto pos = 112 | sse2_strstr_v2(std::string_view(it, haystack_end - it), m_query); 113 | if (pos != std::string::npos) { 114 | it += pos; 115 | } else { 116 | it = haystack_end; 117 | break; 118 | } 119 | } 120 | #else 121 | it = needle_search(m_query, it, haystack_end); 122 | #endif 123 | 124 | if (it != haystack_end) { 125 | // needle found in haystack 126 | 127 | if (!no_file_name) { 128 | if (!printed_file_name) { 129 | if (m_is_stdout) { 130 | // Print filename once, bold cyan color 131 | fmt::format_to( 132 | std::back_inserter(out), "\n\033[1;36m{}\033[0m\n", filename); 133 | } else { 134 | // Print filename without newline, without any color 135 | fmt::format_to(std::back_inserter(out), "{}:", filename); 136 | } 137 | printed_file_name = true; 138 | } else { 139 | if (!m_is_stdout) { 140 | // Print filename for every match 141 | // without any color 142 | fmt::format_to(std::back_inserter(out), "{}:", filename); 143 | } 144 | } 145 | } 146 | 147 | std::string_view line; 148 | 149 | if (m_is_path_from_terminal) { 150 | // Only find lines and count line number if 151 | // this is actually a file 152 | // 153 | // If the input haystack is from a pipe or stdin 154 | // then don't do this 155 | 156 | // Found needle in haystack 157 | auto newline_before = haystack.rfind('\n', it - haystack_begin); 158 | #if defined(__SSE2__) 159 | auto newline_after = haystack_end; 160 | auto pos = 161 | sse2_strstr_v2(std::string_view(it, haystack_end - it), "\n"); 162 | if (pos != std::string::npos) { 163 | newline_after = it + pos; 164 | } 165 | #else 166 | auto newline_after = std::find(it, haystack_end, '\n'); 167 | #endif 168 | // Get line [newline_before, newline_after] 169 | auto line_size = 170 | std::size_t(newline_after - (haystack_begin + newline_before) - 1); 171 | line = haystack.substr(newline_before + 1, line_size); 172 | 173 | // Move to next line and continue search 174 | it = newline_after + 1; 175 | } else { 176 | // Input is from pipe or stdin 177 | // Haystack is one line 178 | // 179 | // Since the processing is done 180 | // Go to end of haystack 181 | line = haystack; 182 | it = haystack_end; 183 | } 184 | 185 | if (m_is_stdout) { 186 | // Print colored, highlight needle in line 187 | print_colored(line, m_query, out); 188 | } else { 189 | fmt::format_to(std::back_inserter(out), "{}\n", line); 190 | } 191 | 192 | first_search = false; 193 | } else { 194 | // no results at all in this file 195 | break; 196 | } 197 | } 198 | 199 | if (!first_search) { 200 | fmt::print("{}", fmt::to_string(out)); 201 | } 202 | 203 | return 0; 204 | } 205 | 206 | std::string get_file_contents(const char* filename) 207 | { 208 | std::FILE* fp = std::fopen(filename, "rb"); 209 | if (fp) { 210 | std::string contents; 211 | std::fseek(fp, 0, SEEK_END); 212 | contents.resize(std::ftell(fp)); 213 | std::rewind(fp); 214 | const auto size = std::fread(&contents[0], 1, contents.size(), fp); 215 | std::fclose(fp); 216 | return (contents); 217 | } 218 | throw(errno); 219 | } 220 | 221 | void searcher::read_file_and_search(const char* path) 222 | { 223 | try { 224 | const std::string haystack = get_file_contents(path); 225 | file_search(path, haystack); 226 | } catch (const std::exception& e) { 227 | } 228 | } 229 | 230 | bool is_whitelisted(const std::string_view& str) 231 | { 232 | static const std::unordered_set allowed_suffixes = { 233 | // ANTLR 234 | ".g4", 235 | // C 236 | ".c", 237 | ".h" 238 | // C++ 239 | ".cpp", 240 | ".cc", 241 | ".cxx", 242 | ".hh", 243 | ".hxx", 244 | ".hpp", 245 | // CUDA 246 | ".cu", 247 | ".cuh", 248 | // Go 249 | ".go", 250 | // Java 251 | ".java", 252 | // JavaScript 253 | ".js", 254 | // Markdown 255 | ".md" 256 | // Python 257 | ".py", 258 | // Rust 259 | ".rs", 260 | ".rs.in", 261 | // Shell 262 | ".sh", 263 | ".bash", 264 | // Text 265 | ".txt", 266 | ".csv", 267 | ".json", 268 | ".xml" 269 | ".yml", 270 | ".yaml", 271 | }; 272 | 273 | bool result = false; 274 | for (const auto& suffix : allowed_suffixes) { 275 | if (str.size() >= suffix.size() 276 | && str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0) 277 | { 278 | result = true; 279 | break; 280 | } 281 | } 282 | return result; 283 | } 284 | 285 | bool exclude_directory(const char* path) 286 | { 287 | static const std::unordered_set ignored_dirs = { 288 | ".git/", ".github/", "build/", 289 | "node_modules/", ".vscode/", ".DS_Store/", 290 | "debugPublic/", "DebugPublic/", "debug/", 291 | "Debug/", "Release/", "release/", 292 | "Releases/", "releases/", "cmake-build-debug/", 293 | "__pycache__/", "Binaries/", "Doc/", 294 | "doc/", "Documentation/", "docs/", 295 | "Docs/", "bin/", "Bin/", 296 | "patches/", "tar-install/", "CMakeFiles/", 297 | "install/", "snap/", "LICENSES/", 298 | "img/", "images/", "imgs/"}; 299 | 300 | for (const auto& ignored_dir : ignored_dirs) { 301 | // if path contains ignored dir, ignore it 302 | if (strstr(path, ignored_dir) != nullptr) { 303 | return true; 304 | } 305 | } 306 | 307 | return false; 308 | } 309 | 310 | int handle_posix_directory_entry(const char* filepath, 311 | const struct stat* info, 312 | const int typeflag, 313 | struct FTW* pathinfo) 314 | { 315 | static const bool skip_fnmatch = 316 | searcher::m_filter == std::string_view {"*.*"}; 317 | 318 | if (typeflag == FTW_DNR) { 319 | // directory not readable 320 | return FTW_SKIP_SUBTREE; 321 | } 322 | 323 | if (typeflag == FTW_D || typeflag == FTW_DP) { 324 | // directory 325 | if (exclude_directory(filepath)) { 326 | return FTW_SKIP_SUBTREE; 327 | } else { 328 | return FTW_CONTINUE; 329 | } 330 | } 331 | 332 | if (typeflag == FTW_F) { 333 | if ((skip_fnmatch && is_whitelisted(filepath)) 334 | || fnmatch(searcher::m_filter.data(), filepath, 0) == 0) 335 | { 336 | searcher::m_ts->push_task( 337 | [pathstring = std::string {filepath}]() 338 | { searcher::read_file_and_search(pathstring.data()); }); 339 | } 340 | } 341 | 342 | return FTW_CONTINUE; 343 | } 344 | 345 | void searcher::directory_search(const char* path) 346 | { 347 | /* Invalid directory path? */ 348 | if (path == NULL || *path == '\0') 349 | return; 350 | 351 | nftw( 352 | path, handle_posix_directory_entry, USE_FDS, FTW_PHYS | FTW_ACTIONRETVAL); 353 | searcher::m_ts->wait_for_tasks(); 354 | } 355 | 356 | } // namespace search 357 | -------------------------------------------------------------------------------- /source/searcher.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | namespace search 24 | { 25 | struct searcher 26 | { 27 | static inline std::unique_ptr m_ts; 28 | static inline std::string_view m_query; 29 | static inline std::string_view m_filter; 30 | static inline bool m_is_stdout; 31 | static inline bool m_is_path_from_terminal; 32 | 33 | static std::size_t file_search(std::string_view filename, 34 | std::string_view haystack); 35 | static void read_file_and_search(const char* path); 36 | static void directory_search(const char* path); 37 | }; 38 | 39 | } // namespace search 40 | -------------------------------------------------------------------------------- /source/sse2_strstr.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #define FORCE_INLINE inline __attribute__((always_inline)) 9 | 10 | #if defined(__AVX2__) 11 | 12 | namespace search 13 | { 14 | namespace 15 | { 16 | bool always_true(const char*, const char*) 17 | { 18 | return true; 19 | } 20 | 21 | bool memcmp1(const char* a, const char* b) 22 | { 23 | return a[0] == b[0]; 24 | } 25 | 26 | bool memcmp2(const char* a, const char* b) 27 | { 28 | const uint16_t A = *reinterpret_cast(a); 29 | const uint16_t B = *reinterpret_cast(b); 30 | return A == B; 31 | } 32 | 33 | bool memcmp3(const char* a, const char* b) 34 | { 35 | const uint32_t A = *reinterpret_cast(a); 36 | const uint32_t B = *reinterpret_cast(b); 37 | return (A & 0x00ffffff) == (B & 0x00ffffff); 38 | } 39 | 40 | bool memcmp4(const char* a, const char* b) 41 | { 42 | const uint32_t A = *reinterpret_cast(a); 43 | const uint32_t B = *reinterpret_cast(b); 44 | return A == B; 45 | } 46 | 47 | bool memcmp5(const char* a, const char* b) 48 | { 49 | const uint64_t A = *reinterpret_cast(a); 50 | const uint64_t B = *reinterpret_cast(b); 51 | return ((A ^ B) & 0x000000fffffffffflu) == 0; 52 | } 53 | 54 | bool memcmp6(const char* a, const char* b) 55 | { 56 | const uint64_t A = *reinterpret_cast(a); 57 | const uint64_t B = *reinterpret_cast(b); 58 | return ((A ^ B) & 0x0000fffffffffffflu) == 0; 59 | } 60 | 61 | bool memcmp7(const char* a, const char* b) 62 | { 63 | const uint64_t A = *reinterpret_cast(a); 64 | const uint64_t B = *reinterpret_cast(b); 65 | return ((A ^ B) & 0x00fffffffffffffflu) == 0; 66 | } 67 | 68 | bool memcmp8(const char* a, const char* b) 69 | { 70 | const uint64_t A = *reinterpret_cast(a); 71 | const uint64_t B = *reinterpret_cast(b); 72 | return A == B; 73 | } 74 | 75 | bool memcmp9(const char* a, const char* b) 76 | { 77 | const uint64_t A = *reinterpret_cast(a); 78 | const uint64_t B = *reinterpret_cast(b); 79 | return (A == B) & (a[8] == b[8]); 80 | } 81 | 82 | bool memcmp10(const char* a, const char* b) 83 | { 84 | const uint64_t Aq = *reinterpret_cast(a); 85 | const uint64_t Bq = *reinterpret_cast(b); 86 | const uint16_t Aw = *reinterpret_cast(a + 8); 87 | const uint16_t Bw = *reinterpret_cast(b + 8); 88 | return (Aq == Bq) & (Aw == Bw); 89 | } 90 | 91 | bool memcmp11(const char* a, const char* b) 92 | { 93 | const uint64_t Aq = *reinterpret_cast(a); 94 | const uint64_t Bq = *reinterpret_cast(b); 95 | const uint32_t Ad = *reinterpret_cast(a + 8); 96 | const uint32_t Bd = *reinterpret_cast(b + 8); 97 | return (Aq == Bq) & ((Ad & 0x00ffffff) == (Bd & 0x00ffffff)); 98 | } 99 | 100 | bool memcmp12(const char* a, const char* b) 101 | { 102 | const uint64_t Aq = *reinterpret_cast(a); 103 | const uint64_t Bq = *reinterpret_cast(b); 104 | const uint32_t Ad = *reinterpret_cast(a + 8); 105 | const uint32_t Bd = *reinterpret_cast(b + 8); 106 | return (Aq == Bq) & (Ad == Bd); 107 | } 108 | 109 | } // namespace 110 | 111 | namespace bits 112 | { 113 | template 114 | T clear_leftmost_set(const T value) 115 | { 116 | assert(value != 0); 117 | 118 | return value & (value - 1); 119 | } 120 | 121 | template 122 | unsigned get_first_bit_set(const T value) 123 | { 124 | assert(value != 0); 125 | 126 | return __builtin_ctz(value); 127 | } 128 | 129 | template<> 130 | unsigned get_first_bit_set(const uint64_t value) 131 | { 132 | assert(value != 0); 133 | 134 | return __builtin_ctzl(value); 135 | } 136 | 137 | } // namespace bits 138 | 139 | size_t FORCE_INLINE sse2_strstr_anysize(const char* s, 140 | size_t n, 141 | const char* needle, 142 | size_t k) 143 | { 144 | assert(k > 0); 145 | assert(n > 0); 146 | 147 | const __m128i first = _mm_set1_epi8(needle[0]); 148 | const __m128i last = _mm_set1_epi8(needle[k - 1]); 149 | 150 | for (size_t i = 0; i < n; i += 16) { 151 | const __m128i block_first = 152 | _mm_loadu_si128(reinterpret_cast(s + i)); 153 | const __m128i block_last = 154 | _mm_loadu_si128(reinterpret_cast(s + i + k - 1)); 155 | 156 | const __m128i eq_first = _mm_cmpeq_epi8(first, block_first); 157 | const __m128i eq_last = _mm_cmpeq_epi8(last, block_last); 158 | 159 | uint16_t mask = _mm_movemask_epi8(_mm_and_si128(eq_first, eq_last)); 160 | 161 | while (mask != 0) { 162 | const auto bitpos = bits::get_first_bit_set(mask); 163 | 164 | if (memcmp(s + i + bitpos + 1, needle + 1, k - 2) == 0) { 165 | return i + bitpos; 166 | } 167 | 168 | mask = bits::clear_leftmost_set(mask); 169 | } 170 | } 171 | 172 | return std::string_view::npos; 173 | } 174 | 175 | // ------------------------------------------------------------------------ 176 | 177 | template 178 | size_t FORCE_INLINE sse2_strstr_memcmp(const char* s, 179 | size_t n, 180 | const char* needle, 181 | MEMCMP memcmp_fun) 182 | { 183 | assert(k > 0); 184 | assert(n > 0); 185 | 186 | const __m128i first = _mm_set1_epi8(needle[0]); 187 | const __m128i last = _mm_set1_epi8(needle[k - 1]); 188 | 189 | for (size_t i = 0; i < n; i += 16) { 190 | const __m128i block_first = 191 | _mm_loadu_si128(reinterpret_cast(s + i)); 192 | const __m128i block_last = 193 | _mm_loadu_si128(reinterpret_cast(s + i + k - 1)); 194 | 195 | const __m128i eq_first = _mm_cmpeq_epi8(first, block_first); 196 | const __m128i eq_last = _mm_cmpeq_epi8(last, block_last); 197 | 198 | uint32_t mask = _mm_movemask_epi8(_mm_and_si128(eq_first, eq_last)); 199 | 200 | while (mask != 0) { 201 | const auto bitpos = bits::get_first_bit_set(mask); 202 | 203 | if (memcmp_fun(s + i + bitpos + 1, needle + 1)) { 204 | return i + bitpos; 205 | } 206 | 207 | mask = bits::clear_leftmost_set(mask); 208 | } 209 | } 210 | 211 | return std::string_view::npos; 212 | } 213 | 214 | // ------------------------------------------------------------------------ 215 | 216 | size_t sse2_strstr_v2(const char* s, size_t n, const char* needle, size_t k) 217 | { 218 | size_t result = std::string_view::npos; 219 | 220 | if (n < k) { 221 | return result; 222 | } 223 | 224 | switch (k) { 225 | case 0: 226 | return 0; 227 | 228 | case 1: { 229 | const char* res = reinterpret_cast(strchr(s, needle[0])); 230 | 231 | return (res != nullptr) ? res - s : std::string_view::npos; 232 | } 233 | 234 | case 2: 235 | result = sse2_strstr_memcmp<2>(s, n, needle, always_true); 236 | break; 237 | 238 | case 3: 239 | result = sse2_strstr_memcmp<3>(s, n, needle, memcmp1); 240 | break; 241 | 242 | case 4: 243 | result = sse2_strstr_memcmp<4>(s, n, needle, memcmp2); 244 | break; 245 | 246 | case 5: 247 | result = sse2_strstr_memcmp<5>(s, n, needle, memcmp4); 248 | break; 249 | 250 | case 6: 251 | result = sse2_strstr_memcmp<6>(s, n, needle, memcmp4); 252 | break; 253 | 254 | case 7: 255 | result = sse2_strstr_memcmp<7>(s, n, needle, memcmp5); 256 | break; 257 | 258 | case 8: 259 | result = sse2_strstr_memcmp<8>(s, n, needle, memcmp6); 260 | break; 261 | 262 | case 9: 263 | result = sse2_strstr_memcmp<9>(s, n, needle, memcmp8); 264 | break; 265 | 266 | case 10: 267 | result = sse2_strstr_memcmp<10>(s, n, needle, memcmp8); 268 | break; 269 | 270 | case 11: 271 | result = sse2_strstr_memcmp<11>(s, n, needle, memcmp9); 272 | break; 273 | 274 | case 12: 275 | result = sse2_strstr_memcmp<12>(s, n, needle, memcmp10); 276 | break; 277 | 278 | default: 279 | result = sse2_strstr_anysize(s, n, needle, k); 280 | break; 281 | } 282 | 283 | if (result <= n - k) { 284 | return result; 285 | } else { 286 | return std::string_view::npos; 287 | } 288 | } 289 | 290 | // ------------------------------------------------------------------------ 291 | 292 | size_t sse2_strstr_v2(const std::string_view& s, const std::string_view& needle) 293 | { 294 | return sse2_strstr_v2(s.data(), s.size(), needle.data(), needle.size()); 295 | } 296 | 297 | } // namespace search 298 | #endif 299 | -------------------------------------------------------------------------------- /source/sse2_strstr.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | #if defined(__SSE2__) 6 | 7 | namespace search 8 | { 9 | size_t sse2_strstr_v2(const std::string_view& s, 10 | const std::string_view& needle); 11 | 12 | } // namespace search 13 | 14 | #endif 15 | -------------------------------------------------------------------------------- /source/thread_pool.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /** 4 | * @file thread_pool.hpp 5 | * @author Barak Shoshany (baraksh@gmail.com) (http://baraksh.com) 6 | * @version 2.0.0 7 | * @date 2021-08-14 8 | * @copyright Copyright (c) 2021 Barak Shoshany. Licensed under the MIT license. 9 | * If you use this library in published research, please cite it as follows: 10 | * - Barak Shoshany, "A C++17 Thread Pool for High-Performance Scientific 11 | * Computing", doi:10.5281/zenodo.4742687, arXiv:2105.00613 (May 2021) 12 | * 13 | * @brief A C++17 thread pool for high-performance scientific computing. 14 | * @details A modern C++17-compatible thread pool implementation, built from 15 | * scratch with high-performance scientific computing in mind. The thread pool 16 | * is implemented as a single lightweight and self-contained class, and does not 17 | * have any dependencies other than the C++17 standard library, thus allowing a 18 | * great degree of portability. In particular, this implementation does not 19 | * utilize OpenMP or any other high-level multithreading APIs, and thus gives 20 | * the programmer precise low-level control over the details of the 21 | * parallelization, which permits more robust optimizations. The thread pool was 22 | * extensively tested on both AMD and Intel CPUs with up to 40 cores and 80 23 | * threads. Other features include automatic generation of futures and easy 24 | * parallelization of loops. Two helper classes enable synchronizing printing to 25 | * an output stream by different threads and measuring execution time for 26 | * benchmarking purposes. Please visit the GitHub repository at 27 | * https://github.com/bshoshany/thread-pool for documentation and updates, or to 28 | * submit feature requests and bug reports. 29 | */ 30 | 31 | #define THREAD_POOL_VERSION "v2.0.0 (2021-08-14)" 32 | 33 | #include // std::atomic 34 | #include // std::chrono 35 | #include // std::int_fast64_t, std::uint_fast32_t 36 | #include // std::function 37 | #include // std::future, std::promise 38 | #include // std::cout, std::ostream 39 | #include // std::shared_ptr, std::unique_ptr 40 | #include // std::mutex, std::scoped_lock 41 | #include // std::queue 42 | #include // std::this_thread, std::thread 43 | #include // std::common_type_t, std::decay_t, std::enable_if_t, std::is_void_v, std::invoke_result_t 44 | #include // std::move 45 | 46 | // ============================================================================================= 47 | // // 48 | // Begin class thread_pool // 49 | 50 | /** 51 | * @brief A C++17 thread pool class. The user submits tasks to be executed into 52 | * a queue. Whenever a thread becomes available, it pops a task from the queue 53 | * and executes it. Each task is automatically assigned a future, which can be 54 | * used to wait for the task to finish executing and/or obtain its eventual 55 | * return value. 56 | */ 57 | class thread_pool 58 | { 59 | typedef std::uint_fast32_t ui32; 60 | typedef std::uint_fast64_t ui64; 61 | 62 | public: 63 | // ============================ 64 | // Constructors and destructors 65 | // ============================ 66 | 67 | /** 68 | * @brief Construct a new thread pool. 69 | * 70 | * @param _thread_count The number of threads to use. The default value is the 71 | * total number of hardware threads available, as reported by the 72 | * implementation. With a hyperthreaded CPU, this will be twice the number of 73 | * CPU cores. If the argument is zero, the default value will be used instead. 74 | */ 75 | thread_pool(const ui32& _thread_count = std::thread::hardware_concurrency()) 76 | : thread_count(_thread_count ? _thread_count 77 | : std::thread::hardware_concurrency()) 78 | , threads(new std::thread[_thread_count 79 | ? _thread_count 80 | : std::thread::hardware_concurrency()]) 81 | { 82 | create_threads(); 83 | } 84 | 85 | /** 86 | * @brief Destruct the thread pool. Waits for all tasks to complete, then 87 | * destroys all threads. Note that if the variable paused is set to true, then 88 | * any tasks still in the queue will never be executed. 89 | */ 90 | ~thread_pool() 91 | { 92 | wait_for_tasks(); 93 | running = false; 94 | destroy_threads(); 95 | } 96 | 97 | // ======================= 98 | // Public member functions 99 | // ======================= 100 | 101 | /** 102 | * @brief Get the number of tasks currently waiting in the queue to be 103 | * executed by the threads. 104 | * 105 | * @return The number of queued tasks. 106 | */ 107 | ui64 get_tasks_queued() const 108 | { 109 | const std::scoped_lock lock(queue_mutex); 110 | return tasks.size(); 111 | } 112 | 113 | /** 114 | * @brief Get the number of tasks currently being executed by the threads. 115 | * 116 | * @return The number of running tasks. 117 | */ 118 | ui32 get_tasks_running() const 119 | { 120 | return tasks_total - (ui32)get_tasks_queued(); 121 | } 122 | 123 | /** 124 | * @brief Get the total number of unfinished tasks - either still in the 125 | * queue, or running in a thread. 126 | * 127 | * @return The total number of tasks. 128 | */ 129 | ui32 get_tasks_total() const 130 | { 131 | return tasks_total; 132 | } 133 | 134 | /** 135 | * @brief Get the number of threads in the pool. 136 | * 137 | * @return The number of threads. 138 | */ 139 | ui32 get_thread_count() const 140 | { 141 | return thread_count; 142 | } 143 | 144 | /** 145 | * @brief Parallelize a loop by splitting it into blocks, submitting each 146 | * block separately to the thread pool, and waiting for all blocks to finish 147 | * executing. The user supplies a loop function, which will be called once per 148 | * block and should iterate over the block's range. 149 | * 150 | * @tparam T1 The type of the first index in the loop. Should be a signed or 151 | * unsigned integer. 152 | * @tparam T2 The type of the index after the last index in the loop. Should 153 | * be a signed or unsigned integer. If T1 is not the same as T2, a common type 154 | * will be automatically inferred. 155 | * @tparam F The type of the function to loop through. 156 | * @param first_index The first index in the loop. 157 | * @param index_after_last The index after the last index in the loop. The 158 | * loop will iterate from first_index to (index_after_last - 1) inclusive. In 159 | * other words, it will be equivalent to "for (T i = first_index; i < 160 | * index_after_last; i++)". Note that if first_index == index_after_last, the 161 | * function will terminate without doing anything. 162 | * @param loop The function to loop through. Will be called once per block. 163 | * Should take exactly two arguments: the first index in the block and the 164 | * index after the last index in the block. loop(start, end) should typically 165 | * involve a loop of the form "for (T i = start; i < end; i++)". 166 | * @param num_blocks The maximum number of blocks to split the loop into. The 167 | * default is to use the number of threads in the pool. 168 | */ 169 | template 170 | void parallelize_loop(const T1& first_index, 171 | const T2& index_after_last, 172 | const F& loop, 173 | ui32 num_blocks = 0) 174 | { 175 | typedef std::common_type_t T; 176 | T the_first_index = (T)first_index; 177 | T last_index = (T)index_after_last; 178 | if (the_first_index == last_index) 179 | return; 180 | if (last_index < the_first_index) { 181 | T temp = last_index; 182 | last_index = the_first_index; 183 | the_first_index = temp; 184 | } 185 | last_index--; 186 | if (num_blocks == 0) 187 | num_blocks = thread_count; 188 | ui64 total_size = (ui64)(last_index - the_first_index + 1); 189 | ui64 block_size = (ui64)(total_size / num_blocks); 190 | if (block_size == 0) { 191 | block_size = 1; 192 | num_blocks = (ui32)total_size > 1 ? (ui32)total_size : 1; 193 | } 194 | std::atomic blocks_running = 0; 195 | for (ui32 t = 0; t < num_blocks; t++) { 196 | T start = ((T)(t * block_size) + the_first_index); 197 | T end = (t == num_blocks - 1) 198 | ? last_index + 1 199 | : ((T)((t + 1) * block_size) + the_first_index); 200 | blocks_running++; 201 | push_task( 202 | [start, end, &loop, &blocks_running] 203 | { 204 | loop(start, end); 205 | blocks_running--; 206 | }); 207 | } 208 | while (blocks_running != 0) { 209 | sleep_or_yield(); 210 | } 211 | } 212 | 213 | /** 214 | * @brief Push a function with no arguments or return value into the task 215 | * queue. 216 | * 217 | * @tparam F The type of the function. 218 | * @param task The function to push. 219 | */ 220 | template 221 | void push_task(const F& task) 222 | { 223 | tasks_total++; 224 | { 225 | const std::scoped_lock lock(queue_mutex); 226 | tasks.push(std::function(task)); 227 | } 228 | } 229 | 230 | /** 231 | * @brief Push a function with arguments, but no return value, into the task 232 | * queue. 233 | * @details The function is wrapped inside a lambda in order to hide the 234 | * arguments, as the tasks in the queue must be of type std::function, 235 | * so they cannot have any arguments or return value. If no arguments are 236 | * provided, the other overload will be used, in order to avoid the (slight) 237 | * overhead of using a lambda. 238 | * 239 | * @tparam F The type of the function. 240 | * @tparam A The types of the arguments. 241 | * @param task The function to push. 242 | * @param args The arguments to pass to the function. 243 | */ 244 | template 245 | void push_task(const F& task, const A&... args) 246 | { 247 | push_task([task, args...] { task(args...); }); 248 | } 249 | 250 | /** 251 | * @brief Reset the number of threads in the pool. Waits for all currently 252 | * running tasks to be completed, then destroys all threads in the pool and 253 | * creates a new thread pool with the new number of threads. Any tasks that 254 | * were waiting in the queue before the pool was reset will then be executed 255 | * by the new threads. If the pool was paused before resetting it, the new 256 | * pool will be paused as well. 257 | * 258 | * @param _thread_count The number of threads to use. The default value is the 259 | * total number of hardware threads available, as reported by the 260 | * implementation. With a hyperthreaded CPU, this will be twice the number of 261 | * CPU cores. If the argument is zero, the default value will be used instead. 262 | */ 263 | void reset(const ui32& _thread_count = std::thread::hardware_concurrency()) 264 | { 265 | bool was_paused = paused; 266 | paused = true; 267 | wait_for_tasks(); 268 | running = false; 269 | destroy_threads(); 270 | thread_count = 271 | _thread_count ? _thread_count : std::thread::hardware_concurrency(); 272 | threads.reset(new std::thread[thread_count]); 273 | paused = was_paused; 274 | running = true; 275 | create_threads(); 276 | } 277 | 278 | /** 279 | * @brief Submit a function with zero or more arguments and no return value 280 | * into the task queue, and get an std::future that will be set to true 281 | * upon completion of the task. 282 | * 283 | * @tparam F The type of the function. 284 | * @tparam A The types of the zero or more arguments to pass to the function. 285 | * @param task The function to submit. 286 | * @param args The zero or more arguments to pass to the function. 287 | * @return A future to be used later to check if the function has finished its 288 | * execution. 289 | */ 290 | template, std::decay_t...>>>> 294 | std::future submit(const F& task, const A&... args) 295 | { 296 | std::shared_ptr> task_promise(new std::promise); 297 | std::future future = task_promise->get_future(); 298 | push_task( 299 | [task, args..., task_promise] 300 | { 301 | try { 302 | task(args...); 303 | task_promise->set_value(true); 304 | } catch (...) { 305 | try { 306 | task_promise->set_exception(std::current_exception()); 307 | } catch (...) { 308 | } 309 | } 310 | }); 311 | return future; 312 | } 313 | 314 | /** 315 | * @brief Submit a function with zero or more arguments and a return value 316 | * into the task queue, and get a future for its eventual returned value. 317 | * 318 | * @tparam F The type of the function. 319 | * @tparam A The types of the zero or more arguments to pass to the function. 320 | * @tparam R The return type of the function. 321 | * @param task The function to submit. 322 | * @param args The zero or more arguments to pass to the function. 323 | * @return A future to be used later to obtain the function's returned value, 324 | * waiting for it to finish its execution if needed. 325 | */ 326 | template< 327 | typename F, 328 | typename... A, 329 | typename R = std::invoke_result_t, std::decay_t...>, 330 | typename = std::enable_if_t>> 331 | std::future submit(const F& task, const A&... args) 332 | { 333 | std::shared_ptr> task_promise(new std::promise); 334 | std::future future = task_promise->get_future(); 335 | push_task( 336 | [task, args..., task_promise] 337 | { 338 | try { 339 | task_promise->set_value(task(args...)); 340 | } catch (...) { 341 | try { 342 | task_promise->set_exception(std::current_exception()); 343 | } catch (...) { 344 | } 345 | } 346 | }); 347 | return future; 348 | } 349 | 350 | /** 351 | * @brief Wait for tasks to be completed. Normally, this function waits for 352 | * all tasks, both those that are currently running in the threads and those 353 | * that are still waiting in the queue. However, if the variable paused is set 354 | * to true, this function only waits for the currently running tasks 355 | * (otherwise it would wait forever). To wait for a specific task, use 356 | * submit() instead, and call the wait() member function of the generated 357 | * future. 358 | */ 359 | void wait_for_tasks() 360 | { 361 | while (true) { 362 | if (!paused) { 363 | if (tasks_total == 0) 364 | break; 365 | } else { 366 | if (get_tasks_running() == 0) 367 | break; 368 | } 369 | sleep_or_yield(); 370 | } 371 | } 372 | 373 | // =========== 374 | // Public data 375 | // =========== 376 | 377 | /** 378 | * @brief An atomic variable indicating to the workers to pause. When set to 379 | * true, the workers temporarily stop popping new tasks out of the queue, 380 | * although any tasks already executed will keep running until they are done. 381 | * Set to false again to resume popping tasks. 382 | */ 383 | std::atomic paused = false; 384 | 385 | /** 386 | * @brief The duration, in microseconds, that the worker function should sleep 387 | * for when it cannot find any tasks in the queue. If set to 0, then instead 388 | * of sleeping, the worker function will execute std::this_thread::yield() if 389 | * there are no tasks in the queue. The default value is 1000. 390 | */ 391 | ui32 sleep_duration = 1000; 392 | 393 | private: 394 | // ======================== 395 | // Private member functions 396 | // ======================== 397 | 398 | /** 399 | * @brief Create the threads in the pool and assign a worker to each thread. 400 | */ 401 | void create_threads() 402 | { 403 | for (ui32 i = 0; i < thread_count; i++) { 404 | threads[i] = std::thread(&thread_pool::worker, this); 405 | } 406 | } 407 | 408 | /** 409 | * @brief Destroy the threads in the pool by joining them. 410 | */ 411 | void destroy_threads() 412 | { 413 | for (ui32 i = 0; i < thread_count; i++) { 414 | threads[i].join(); 415 | } 416 | } 417 | 418 | /** 419 | * @brief Try to pop a new task out of the queue. 420 | * 421 | * @param task A reference to the task. Will be populated with a function if 422 | * the queue is not empty. 423 | * @return true if a task was found, false if the queue is empty. 424 | */ 425 | bool pop_task(std::function& task) 426 | { 427 | const std::scoped_lock lock(queue_mutex); 428 | if (tasks.empty()) 429 | return false; 430 | else { 431 | task = std::move(tasks.front()); 432 | tasks.pop(); 433 | return true; 434 | } 435 | } 436 | 437 | /** 438 | * @brief Sleep for sleep_duration microseconds. If that variable is set to 439 | * zero, yield instead. 440 | * 441 | */ 442 | void sleep_or_yield() 443 | { 444 | if (sleep_duration) 445 | std::this_thread::sleep_for(std::chrono::microseconds(sleep_duration)); 446 | else 447 | std::this_thread::yield(); 448 | } 449 | 450 | /** 451 | * @brief A worker function to be assigned to each thread in the pool. 452 | * Continuously pops tasks out of the queue and executes them, as long as the 453 | * atomic variable running is set to true. 454 | */ 455 | void worker() 456 | { 457 | while (running) { 458 | std::function task; 459 | if (!paused && pop_task(task)) { 460 | task(); 461 | tasks_total--; 462 | } else { 463 | sleep_or_yield(); 464 | } 465 | } 466 | } 467 | 468 | // ============ 469 | // Private data 470 | // ============ 471 | 472 | /** 473 | * @brief A mutex to synchronize access to the task queue by different 474 | * threads. 475 | */ 476 | mutable std::mutex queue_mutex = {}; 477 | 478 | /** 479 | * @brief An atomic variable indicating to the workers to keep running. When 480 | * set to false, the workers permanently stop working. 481 | */ 482 | std::atomic running = true; 483 | 484 | /** 485 | * @brief A queue of tasks to be executed by the threads. 486 | */ 487 | std::queue> tasks = {}; 488 | 489 | /** 490 | * @brief The number of threads in the pool. 491 | */ 492 | ui32 thread_count; 493 | 494 | /** 495 | * @brief A smart pointer to manage the memory allocated for the threads. 496 | */ 497 | std::unique_ptr threads; 498 | 499 | /** 500 | * @brief An atomic variable to keep track of the total number of unfinished 501 | * tasks - either still in the queue, or running in a thread. 502 | */ 503 | std::atomic tasks_total = 0; 504 | }; 505 | 506 | // End class thread_pool // 507 | // ============================================================================================= 508 | // // 509 | 510 | // ============================================================================================= 511 | // // 512 | // Begin class synced_stream // 513 | 514 | /** 515 | * @brief A helper class to synchronize printing to an output stream by 516 | * different threads. 517 | */ 518 | class synced_stream 519 | { 520 | public: 521 | /** 522 | * @brief Construct a new synced stream. 523 | * 524 | * @param _out_stream The output stream to print to. The default value is 525 | * std::cout. 526 | */ 527 | synced_stream(std::ostream& _out_stream = std::cout) 528 | : out_stream(_out_stream) {}; 529 | 530 | /** 531 | * @brief Print any number of items into the output stream. Ensures that no 532 | * other threads print to this stream simultaneously, as long as they all 533 | * exclusively use this synced_stream object to print. 534 | * 535 | * @tparam T The types of the items 536 | * @param items The items to print. 537 | */ 538 | template 539 | void print(const T&... items) 540 | { 541 | const std::scoped_lock lock(stream_mutex); 542 | (out_stream << ... << items); 543 | } 544 | 545 | /** 546 | * @brief Print any number of items into the output stream, followed by a 547 | * newline character. Ensures that no other threads print to this stream 548 | * simultaneously, as long as they all exclusively use this synced_stream 549 | * object to print. 550 | * 551 | * @tparam T The types of the items 552 | * @param items The items to print. 553 | */ 554 | template 555 | void println(const T&... items) 556 | { 557 | print(items..., '\n'); 558 | } 559 | 560 | private: 561 | /** 562 | * @brief A mutex to synchronize printing. 563 | */ 564 | mutable std::mutex stream_mutex = {}; 565 | 566 | /** 567 | * @brief The output stream to print to. 568 | */ 569 | std::ostream& out_stream; 570 | }; 571 | 572 | // End class synced_stream // 573 | // ============================================================================================= 574 | // // 575 | 576 | // ============================================================================================= 577 | // // 578 | // Begin class timer // 579 | 580 | /** 581 | * @brief A helper class to measure execution time for benchmarking purposes. 582 | */ 583 | class timer 584 | { 585 | typedef std::int_fast64_t i64; 586 | 587 | public: 588 | /** 589 | * @brief Start (or restart) measuring time. 590 | */ 591 | void start() 592 | { 593 | start_time = std::chrono::steady_clock::now(); 594 | } 595 | 596 | /** 597 | * @brief Stop measuring time and store the elapsed time since start(). 598 | */ 599 | void stop() 600 | { 601 | elapsed_time = std::chrono::steady_clock::now() - start_time; 602 | } 603 | 604 | /** 605 | * @brief Get the number of milliseconds that have elapsed between start() and 606 | * stop(). 607 | * 608 | * @return The number of milliseconds. 609 | */ 610 | i64 ms() const 611 | { 612 | return (std::chrono::duration_cast(elapsed_time)) 613 | .count(); 614 | } 615 | 616 | private: 617 | /** 618 | * @brief The time point when measuring started. 619 | */ 620 | std::chrono::time_point start_time = 621 | std::chrono::steady_clock::now(); 622 | 623 | /** 624 | * @brief The duration that has elapsed between start() and stop(). 625 | */ 626 | std::chrono::duration elapsed_time = 627 | std::chrono::duration::zero(); 628 | }; 629 | 630 | // End class timer // 631 | // ============================================================================================= 632 | // // 633 | -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Parent project does not export its library target, so this CML implicitly 2 | # depends on being added from it, i.e. the testing is done only from the build 3 | # tree and is not feasible from an install location 4 | 5 | project(oystrTests LANGUAGES CXX) 6 | 7 | # ---- Tests ---- 8 | 9 | add_executable(oystr_test source/oystr_test.cpp) 10 | target_link_libraries(oystr_test PRIVATE oystr_lib) 11 | target_compile_features(oystr_test PRIVATE cxx_std_17) 12 | 13 | add_test(NAME oystr_test COMMAND oystr_test) 14 | 15 | # ---- End-of-file commands ---- 16 | 17 | add_folders(Test) 18 | -------------------------------------------------------------------------------- /test/source/oystr_test.cpp: -------------------------------------------------------------------------------- 1 | auto main() -> int 2 | { 3 | return 0; 4 | } 5 | --------------------------------------------------------------------------------