diff --git a/.github/workflows/meson.yml b/.github/workflows/meson.yml new file mode 100644 index 00000000..cede0ba5 --- /dev/null +++ b/.github/workflows/meson.yml @@ -0,0 +1,171 @@ +on: + workflow_call: + +jobs: + meson: + runs-on: ${{ matrix.platform }} + name: meson on ${{ matrix.platform }} (${{ matrix.mode.name }}) + strategy: + fail-fast: false + matrix: + mode: + - name: default + args: -Dtests=enabled -Dbenchmarks=disabled + extra_envs: {} + + # Alternative compiler setups + - name: gcc + args: -Dtests=enabled -Dbenchmarks=disabled + extra_envs: + CC: gcc + CXX: g++ + - name: clang + args: -Dtests=enabled -Dbenchmarks=disabled + extra_envs: + CC: clang + CXX: clang++ + + - name: sanitize + args: >- + -Dtests=enabled -Dbenchmarks=disabled "-Db_sanitize=address,undefined" + extra_envs: {} + + # This is for MSVC, which only supports AddressSanitizer. + # https://learn.microsoft.com/en-us/cpp/sanitizers/ + - name: sanitize+asanonly + args: -Dtests=enabled -Dbenchmarks=disabled -Db_sanitize=address + extra_envs: + ASAN_OPTIONS: report_globals=0:halt_on_error=1:abort_on_error=1:print_summary=1 + + - name: clang+sanitize + args: >- + -Dtests=enabled -Dbenchmarks=disabled -Db_lundef=false "-Db_sanitize=address,undefined" + extra_envs: + CC: clang + CXX: clang++ + + # default clang on GitHub hosted runners is from MSYS2. + # Use Visual Studio supplied clang-cl instead. + - name: clang-cl + args: >- + -Dtests=enabled -Dbenchmarks=disabled + extra_envs: + CC: clang-cl + CXX: clang-cl + - name: clang-cl+sanitize + args: >- + -Dtests=enabled -Dbenchmarks=disabled "-Db_sanitize=address,undefined" + extra_envs: + CC: clang-cl + CXX: clang-cl + platform: + - ubuntu-24.04 + - windows-2025 + - macos-15 + + exclude: + # clang-cl only makes sense on windows. + - platform: ubuntu-24.04 + mode: + name: clang-cl + - platform: macos-15 + mode: + name: clang-cl + - platform: ubuntu-24.04 + mode: + name: clang-cl+sanitize + - platform: macos-15 + mode: + name: clang-cl+sanitize + + # FIXME: clang-cl is currently broken: + # https://github.com/llvm/llvm-project/issues/143245 + - platform: windows-2025 + mode: + name: clang-cl + - platform: windows-2025 + mode: + name: clang-cl+sanitize + + # Use clang-cl instead of MSYS2 clang. + # + # we already tested clang+sanitize on linux, + # if this doesn't work, it should be an issue for MSYS2 team to consider. + - platform: windows-2025 + mode: + name: clang + - platform: windows-2025 + mode: + name: clang+sanitize + + # MSVC-only sanitizers + - platform: ubuntu-24.04 + mode: + name: sanitize+asanonly + - platform: macos-15 + mode: + name: sanitize+asanonly + - platform: windows-2025 + mode: + name: sanitize + + # clang is the default on macos + # also gcc is an alias to clang + - platform: macos-15 + mode: + name: clang + - platform: macos-15 + mode: + name: gcc + + # gcc is the default on linux + - platform: ubuntu-24.04 + mode: + name: gcc + + # MinGW is not officially tested + - platform: windows-2025 + mode: + name: gcc + + # only run sanitizer tests on linux + # + # gcc/clang's codegen shouldn't massively change across platforms, + # and linux supports most of the sanitizers. + - platform: macos-15 + mode: + name: clang+sanitize + - platform: macos-15 + mode: + name: sanitize + + steps: + - name: Setup meson + run: | + pipx install meson ninja + - name: Checkout + uses: actions/checkout@v4 + - name: Activate MSVC and Configure + if: ${{ matrix.platform == 'windows-2025' }} + env: ${{ matrix.mode.extra_envs }} + run: | + meson setup build --buildtype=release ${{ matrix.mode.args }} --vsenv + - name: Configuring + if: ${{ matrix.platform != 'windows-2025' }} + env: ${{ matrix.mode.extra_envs }} + run: | + meson setup build --buildtype=release ${{ matrix.mode.args }} + - name: Building + run: | + meson compile -C build + meson compile -C build msft_proxy_tests examples + + - name: Running tests + env: ${{ matrix.mode.extra_envs }} + run: | + meson test -C build --timeout-multiplier 5 --print-errorlogs + - uses: actions/upload-artifact@v4 + if: ${{ failure() }} + with: + name: ${{ matrix.platform }}-${{ matrix.mode.name }}-logs + path: build/meson-logs diff --git a/.github/workflows/pipeline-ci.yml b/.github/workflows/pipeline-ci.yml index f8037dce..7f311145 100644 --- a/.github/workflows/pipeline-ci.yml +++ b/.github/workflows/pipeline-ci.yml @@ -41,6 +41,10 @@ jobs: name: Generate report needs: [run-bvt-gcc, run-bvt-clang, run-bvt-msvc, run-bvt-appleclang, run-bvt-nvhpc, run-bvt-oneapi] + run-meson: + uses: ./.github/workflows/meson.yml + name: Run builds with meson + mkdocs: uses: ./.github/workflows/mkdocs.yml name: Build mkdocs diff --git a/.github/workflows/pipeline-release.yml b/.github/workflows/pipeline-release.yml index 67778741..77a9d2c8 100644 --- a/.github/workflows/pipeline-release.yml +++ b/.github/workflows/pipeline-release.yml @@ -17,8 +17,8 @@ jobs: version=$(grep -oP 'msft_proxy\d+\s+VERSION\s+\K[0-9]+\.[0-9]+\.[0-9]+' CMakeLists.txt) git tag "$version" git push origin "$version" - tar -czf "proxy-$version.tgz" $(git ls-files 'include/**.h' 'include/**.ixx') - echo "PRO_VER=$version" >> $GITHUB_OUTPUT + git ls-files 'include/**.h' 'include/**.ixx' | xargs tar -czf "proxy-$version.tgz" + echo "PRO_VER=$version" >> "$GITHUB_OUTPUT" shell: bash - name: create release draft diff --git a/.gitignore b/.gitignore index 631c2447..d2e81b70 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,10 @@ # Ignore build directories build/ Testing/ + +# Python bytecode cache +__pycache__/ + +# Meson subprojects +/subprojects/* +!/subprojects/*.wrap diff --git a/benchmarks/meson.build b/benchmarks/meson.build new file mode 100644 index 00000000..a607ed35 --- /dev/null +++ b/benchmarks/meson.build @@ -0,0 +1,80 @@ +benchmarks_exe = executable( + 'msft_proxy_benchmarks', + files( + 'proxy_creation_benchmark.cpp', + 'proxy_operation_benchmark.cpp', + 'proxy_operation_benchmark_context.cpp', + ), + implicit_include_directories: false, + dependencies: [msft_proxy4_dep, benchmark_dep], + build_by_default: false, +) + +autogen_benchmarks = custom_target( + command: [ + autogen_benchmarks_exe, + custom_target( + command: [benchmarks_exe, '--benchmark_list_tests'], + capture: true, + output: 'tests.txt', + ), + meson.current_source_dir() / 'meson.build', + ], + output: 'autogen.stamp', + build_by_default: true, +) + +#pragma autogen push +benchmarks = { + 'ObjectCreation': [ + 'BM_SmallObjectCreationWithProxy_Shared', + 'BM_SmallObjectCreationWithProxy_SharedPooled', + 'BM_SmallObjectCreationWithUniquePtr', + 'BM_SmallObjectCreationWithSharedPtr', + 'BM_SmallObjectCreationWithSharedPtr_Pooled', + 'BM_SmallObjectCreationWithAny', + 'BM_LargeObjectCreationWithProxy', + 'BM_LargeObjectCreationWithProxy_Pooled', + 'BM_LargeObjectCreationWithProxy_Shared', + 'BM_LargeObjectCreationWithProxy_SharedPooled', + 'BM_LargeObjectCreationWithUniquePtr', + 'BM_LargeObjectCreationWithSharedPtr', + 'BM_LargeObjectCreationWithSharedPtr_Pooled', + 'BM_LargeObjectCreationWithAny', + ], + 'ObjectInvocation': [ + 'BM_SmallObjectInvocationViaProxy_Shared', + 'BM_SmallObjectInvocationViaProxyView', + 'BM_SmallObjectInvocationViaVirtualFunction', + 'BM_SmallObjectInvocationViaVirtualFunction_Shared', + 'BM_SmallObjectInvocationViaVirtualFunction_RawPtr', + 'BM_LargeObjectInvocationViaProxy', + 'BM_LargeObjectInvocationViaProxy_Shared', + 'BM_LargeObjectInvocationViaProxyView', + 'BM_LargeObjectInvocationViaVirtualFunction', + 'BM_LargeObjectInvocationViaVirtualFunction_Shared', + 'BM_LargeObjectInvocationViaVirtualFunction_RawPtr', + ], + 'ObjectRelocation': [ + 'BM_SmallObjectRelocationViaProxy_NothrowRelocatable', + 'BM_SmallObjectRelocationViaUniquePtr', + 'BM_SmallObjectRelocationViaAny', + 'BM_LargeObjectRelocationViaProxy', + 'BM_LargeObjectRelocationViaProxy_NothrowRelocatable', + 'BM_LargeObjectRelocationViaUniquePtr', + 'BM_LargeObjectRelocationViaAny', + ], +} +#pragma autogen pop + +foreach suite : benchmarks.keys() + foreach name : benchmarks[suite] + benchmark( + name, + benchmarks_exe, + args: [f'--benchmark_filter=^@name@$'], + suite: suite, + depends: autogen_benchmarks, + ) + endforeach +endforeach diff --git a/docs/example.cpp.in b/docs/example.cpp.in new file mode 100644 index 00000000..1f228572 --- /dev/null +++ b/docs/example.cpp.in @@ -0,0 +1,4 @@ +// This file was auto-generated from: +// @MD_PATH@ + +@CODE@ diff --git a/docs/meson.build b/docs/meson.build new file mode 100644 index 00000000..dc496c02 --- /dev/null +++ b/docs/meson.build @@ -0,0 +1,112 @@ +maybe_fmt_dep = dependency( + 'fmt', + version: fmt_spec, + required: false, + disabler: true, +) + +autogen_glob = custom_target( + command: [autogen_doctest_exe, meson.current_source_dir()], + output: 'autogen.stamp', + build_by_default: true, +) + +#pragma autogen push +docs = [ + 'spec/PRO_DEF_FREE_AS_MEM_DISPATCH.md', + 'spec/PRO_DEF_FREE_DISPATCH.md', + 'spec/PRO_DEF_MEM_DISPATCH.md', + 'spec/allocate_proxy.md', + 'spec/allocate_proxy_shared.md', + 'spec/facade_aware_overload_t.md', + 'spec/inplace_proxiable_target.md', + 'spec/is_bitwise_trivially_relocatable.md', + 'spec/make_proxy.md', + 'spec/make_proxy_inplace.md', + 'spec/make_proxy_shared.md', + 'spec/make_proxy_view.md', + 'spec/msft_lib_proxy.md', + 'spec/proxiable.md', + 'spec/proxiable_target.md', + 'spec/proxy_invoke.md', + 'spec/proxy_reflect.md', + 'spec/proxy_view.md', + 'spec/skills_as_view.md', + 'spec/skills_as_weak.md', + 'spec/skills_fmt_format.md', + 'spec/skills_format.md', + 'spec/skills_slim.md', + 'spec/weak_proxy.md', + 'spec/basic_facade_builder/README.md', + 'spec/basic_facade_builder/add_convention.md', + 'spec/basic_facade_builder/add_facade.md', + 'spec/basic_facade_builder/add_reflection.md', + 'spec/basic_facade_builder/add_skill.md', + 'spec/basic_facade_builder/build.md', + 'spec/basic_facade_builder/restrict_layout.md', + 'spec/basic_facade_builder/support_copy.md', + 'spec/basic_facade_builder/support_destruction.md', + 'spec/basic_facade_builder/support_relocation.md', + 'spec/explicit_conversion_dispatch/README.md', + 'spec/implicit_conversion_dispatch/README.md', + 'spec/operator_dispatch/README.md', + 'spec/proxy/README.md', + 'spec/proxy/constructor.md', + 'spec/proxy/destructor.md', + 'spec/proxy/emplace.md', + 'spec/proxy/friend_operator_equality.md', + 'spec/proxy/friend_swap.md', + 'spec/proxy/indirection.md', + 'spec/proxy/operator_bool.md', + 'spec/proxy/reset.md', + 'spec/skills_rtti/README.md', + 'spec/skills_rtti/proxy_cast.md', + 'spec/skills_rtti/proxy_typeid.md', + 'spec/substitution_dispatch/README.md', + 'spec/weak_dispatch/README.md', +] +#pragma autogen pop + +docdir = get_option('docdir') +if docdir == '' + docdir = get_option('datadir') / 'doc' / meson.project_name() +endif +install_data( + docs, + follow_symlinks: true, + install_dir: docdir, + preserve_path: true, + install_tag: 'docs', +) + +examples = [] + +foreach doc : docs + deps = [msft_proxy4_dep] + if doc.contains('fmt') + deps += maybe_fmt_dep + endif + + name = doc.replace('/', '_').substring(0, -3) + example_exe = executable( + f'example_@name@', + custom_target( + command: [extract_doctest_exe, '@INPUT@', '@OUTPUT@'], + input: doc, + output: f'@name@.cpp', + ), + extra_files: doc, + implicit_include_directories: false, + dependencies: deps, + build_by_default: false, + ) + examples += example_exe + test( + name, + example_exe, + suite: 'ProxyExamples', + depends: autogen_glob, + ) +endforeach + +alias_target('examples', examples) diff --git a/include/proxy/v4/proxy_fmt.h b/include/proxy/v4/proxy_fmt.h index 7d2c4839..531b7b36 100644 --- a/include/proxy/v4/proxy_fmt.h +++ b/include/proxy/v4/proxy_fmt.h @@ -11,7 +11,12 @@ #error Please ensure that proxy.h is included before proxy_fmt.h. #endif // __msft_lib_proxy4 -#if FMT_VERSION >= 60100 +#if FMT_VERSION >= 120000 +#ifndef FMT_XCHAR_H_ +#error The {fmt} library must have wchar_t support enabled. \ +Include fmt/xchar.h before including proxy_fmt.h. +#endif +#elif FMT_VERSION >= 60100 static_assert(fmt::is_char::value, "The {fmt} library must have wchar_t support enabled. " "Include fmt/xchar.h before including proxy_fmt.h."); diff --git a/meson.build b/meson.build new file mode 100644 index 00000000..ccda3d38 --- /dev/null +++ b/meson.build @@ -0,0 +1,139 @@ +project( + 'msft_proxy4', + 'cpp', + version: '4.0.1', + license: 'MIT', + license_files: 'LICENSE', + meson_version: '>=1.3', + default_options: { + 'cpp_std': ['vc++latest', 'c++26', 'c++23', 'c++20'], + 'warning_level': '3', + }, +) + +cxx = meson.get_compiler('cpp') +if cxx.get_id() in ['clang-cl', 'intel-llvm-cl'] + warning( + '[[msvc::no_unique_address]] is broken on clang. Proxy probably won\'t work at all.', + 'https://github.com/llvm/llvm-project/issues/143245' + ) +endif +if cxx.get_argument_syntax() == 'msvc' + add_project_arguments('/bigobj', language: 'cpp') +endif + +inc = include_directories('include') + +hdrs = files( + 'include/proxy/proxy.h', + 'include/proxy/proxy_fmt.h', + 'include/proxy/proxy_macros.h', +) +hdrs_v4 = files( + 'include/proxy/v4/proxy.h', + 'include/proxy/v4/proxy_fmt.h', + 'include/proxy/v4/proxy_macros.h', +) +hdrs_mod = files('include/proxy/v4/proxy.ixx') + +msft_proxy4_dep = declare_dependency( + sources: hdrs + hdrs_v4, + include_directories: inc, +) +modules = get_option('modules') +modules_interface_spec = { + 'gcc': { + 'flags': [ + '-fmodules', + '-fdeps-format=p1689r5', + '-DWINPTHREAD_COND_DECL=WINPTHREADS_ALWAYS_INLINE', + ], + 'check_only': ['-M'], + }, + 'msvc': { + 'flags': ['/interface'], + }, +}[cxx.get_argument_syntax()] +if cxx.has_multi_arguments( + modules_interface_spec.get('flags'), + modules_interface_spec.get('check_only', []), + required: modules, +) + modules_interface_cflags = modules_interface_spec['flags'] + msft_proxy4_mod = declare_dependency( + include_directories: inc, + dependencies: msft_proxy4_dep, + link_with: static_library( + 'msft_proxy4_module', + hdrs_mod, + include_directories: inc, + cpp_args: modules_interface_cflags, + implicit_include_directories: false, + build_by_default: true, + ), + ) +else + modules_interface_cflags = [] + msft_proxy4_mod = disabler() +endif + +install_headers( + hdrs, + subdir: 'proxy', +) +install_headers( + hdrs_v4, + subdir: 'proxy/v4', +) +install_headers( + hdrs_mod, + subdir: 'proxy/v4', +) +pkgconfig = import( + 'pkgconfig', + required: false, +) +if pkgconfig.found() + pkgconfig.generate( + name: meson.project_name(), + description: 'Next Generation Polymorphism in C++', + url: 'https://microsoft.github.io/proxy/', + ) +endif + +meson.override_dependency(meson.project_name(), msft_proxy4_dep) +meson.override_dependency(meson.project_name() + '_module', msft_proxy4_mod) + +subdir('tools') + +tests = get_option('tests') +gtest_dep = dependency( + 'gtest_main', + main: true, + required: tests, +) +fmt_spec = ['>=6.1.0'] +fmt_dep = dependency( + 'fmt', + version: fmt_spec, + required: tests, +) +subdir( + 'tests', + if_found: [gtest_dep, fmt_dep], +) + +benchmarks = get_option('benchmarks') +benchmark_dep = dependency( + 'benchmark_main', + required: benchmarks, +) +subdir( + 'benchmarks', + if_found: benchmark_dep, +) + +examples = get_option('examples').disable_auto_if(meson.is_subproject()) +if examples.allowed() + subdir('docs') +endif diff --git a/meson.format b/meson.format new file mode 100644 index 00000000..66e2f726 --- /dev/null +++ b/meson.format @@ -0,0 +1,4 @@ +indent_by=' ' +end_of_line=lf +insert_final_newline=true +kwargs_force_multiline=true diff --git a/meson.options b/meson.options new file mode 100644 index 00000000..2d88e8ed --- /dev/null +++ b/meson.options @@ -0,0 +1,35 @@ +option( + 'tests', + type: 'feature', + value: 'auto', + description: 'Build tests', +) +option( + 'tests_freestanding', + type: 'feature', + value: 'auto', + description: 'Build freestanding tests', +) +option( + 'benchmarks', + type: 'feature', + value: 'auto', + description: 'Build benchmarks', +) +option( + 'examples', + type: 'feature', + value: 'auto', + description: 'Extract and build examples from docs', +) +option( + 'modules', + type: 'feature', + value: 'auto', + description: 'Build with C++ modules support', +) +option( + 'docdir', + type: 'string', + description: 'Documentation directory', +) diff --git a/subprojects/fmt.wrap b/subprojects/fmt.wrap new file mode 100644 index 00000000..7e9c5c42 --- /dev/null +++ b/subprojects/fmt.wrap @@ -0,0 +1,13 @@ +[wrap-file] +directory = fmt-12.0.0 +source_url = https://github.com/fmtlib/fmt/archive/12.0.0.tar.gz +source_filename = fmt-12.0.0.tar.gz +source_hash = aa3e8fbb6a0066c03454434add1f1fc23299e85758ceec0d7d2d974431481e40 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/fmt_12.0.0-1/fmt-12.0.0.tar.gz +patch_filename = fmt_12.0.0-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/fmt_12.0.0-1/get_patch +patch_hash = 307f288ebf3850abf2f0c50ac1fb07de97df9538d39146d802f3c0d6cada8998 +wrapdb_version = 12.0.0-1 + +[provide] +dependency_names = fmt diff --git a/subprojects/google-benchmark.wrap b/subprojects/google-benchmark.wrap new file mode 100644 index 00000000..7e2e6e73 --- /dev/null +++ b/subprojects/google-benchmark.wrap @@ -0,0 +1,13 @@ +[wrap-file] +directory = benchmark-1.8.4 +source_url = https://github.com/google/benchmark/archive/refs/tags/v1.8.4.tar.gz +source_filename = benchmark-1.8.4.tar.gz +source_hash = 3e7059b6b11fb1bbe28e33e02519398ca94c1818874ebed18e504dc6f709be45 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/google-benchmark_1.8.4-5/benchmark-1.8.4.tar.gz +patch_filename = google-benchmark_1.8.4-5_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/google-benchmark_1.8.4-5/get_patch +patch_hash = 671ffed65f1e95e8c20edb7a06eb54476797e58169160b255f52dc71f4b83957 +wrapdb_version = 1.8.4-5 + +[provide] +dependency_names = benchmark, benchmark_main diff --git a/subprojects/gtest.wrap b/subprojects/gtest.wrap new file mode 100644 index 00000000..9902a4f7 --- /dev/null +++ b/subprojects/gtest.wrap @@ -0,0 +1,16 @@ +[wrap-file] +directory = googletest-1.17.0 +source_url = https://github.com/google/googletest/archive/refs/tags/v1.17.0.tar.gz +source_filename = googletest-1.17.0.tar.gz +source_hash = 65fab701d9829d38cb77c14acdc431d2108bfdbf8979e40eb8ae567edf10b27c +patch_filename = gtest_1.17.0-4_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/gtest_1.17.0-4/get_patch +patch_hash = 3abf7662d09db706453a5b064a1e914678c74b9d9b0b19382747ca561d0d8750 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/gtest_1.17.0-4/googletest-1.17.0.tar.gz +wrapdb_version = 1.17.0-4 + +[provide] +gtest = gtest_dep +gtest_main = gtest_main_dep +gmock = gmock_dep +gmock_main = gmock_main_dep diff --git a/tests/meson.build b/tests/meson.build new file mode 100644 index 00000000..4f185e1b --- /dev/null +++ b/tests/meson.build @@ -0,0 +1,347 @@ +tests_exe = executable( + 'msft_proxy_tests', + files( + 'proxy_creation_tests.cpp', + 'proxy_dispatch_tests.cpp', + 'proxy_fmt_format_tests.cpp', + 'proxy_format_tests.cpp', + 'proxy_integration_tests.cpp', + 'proxy_invocation_tests.cpp', + 'proxy_lifetime_tests.cpp', + 'proxy_reflection_tests.cpp', + 'proxy_regression_tests.cpp', + 'proxy_rtti_tests.cpp', + 'proxy_traits_tests.cpp', + 'proxy_view_tests.cpp', + ), + implicit_include_directories: false, + dependencies: [msft_proxy4_dep, gtest_dep, fmt_dep], + build_by_default: false, +) + +autogen_tests = custom_target( + command: [ + autogen_tests_exe, + custom_target( + command: [tests_exe, '--gtest_list_tests', '--gtest_output=json:@OUTPUT@'], + output: 'tests.json', + ), + meson.current_source_dir() / 'meson.build', + ], + output: 'autogen.stamp', + build_by_default: true, +) + +#pragma autogen push +tests = { + 'ProxyCreationTests': [ + 'TestMakeProxyInplace_FromValue', + 'TestMakeProxyInplace_InPlace', + 'TestMakeProxyInplace_InPlaceInitializerList', + 'TestMakeProxyInplace_Lifetime_Copy', + 'TestMakeProxyInplace_Lifetime_Move', + 'TestAllocateProxy_DirectAllocator_FromValue', + 'TestAllocateProxy_DirectAllocator_InPlace', + 'TestAllocateProxy_DirectAllocator_InPlaceInitializerList', + 'TestAllocateProxy_DirectAllocator_Lifetime_Copy', + 'TestAllocateProxy_DirectAllocator_Lifetime_Move', + 'TestAllocateProxy_IndirectAllocator_FromValue', + 'TestAllocateProxy_IndirectAllocator_InPlace', + 'TestAllocateProxy_IndirectAllocator_InPlaceInitializerList', + 'TestAllocateProxy_IndirectAllocator_Lifetime_Copy', + 'TestAllocateProxy_IndirectAllocator_Lifetime_Move', + 'TestMakeProxy_WithSBO_FromValue', + 'TestMakeProxy_WithSBO_InPlace', + 'TestMakeProxy_WithSBO_InPlaceInitializerList', + 'TestMakeProxy_WithSBO_Lifetime_Copy', + 'TestMakeProxy_WithSBO_Lifetime_Move', + 'TestMakeProxy_WithoutSBO_FromValue', + 'TestMakeProxy_WithoutSBO_InPlace', + 'TestMakeProxy_WithoutSBO_InPlaceInitializerList', + 'TestMakeProxy_WithoutSBO_Lifetime_Copy', + 'TestMakeProxy_WithoutSBO_Lifetime_Move', + 'TestMakeProxy_SfinaeUnsafe', + 'TestAllocateProxyShared_SharedCompact_FromValue', + 'TestAllocateProxyShared_SharedCompact_InPlace', + 'TestAllocateProxyShared_SharedCompact_InPlaceInitializerList', + 'TestAllocateProxyShared_SharedCompact_Lifetime_Copy', + 'TestAllocateProxyShared_SharedCompact_Lifetime_Move', + 'TestAllocateProxyShared_StrongCompact_FromValue', + 'TestAllocateProxyShared_StrongCompact_InPlace', + 'TestAllocateProxyShared_StrongCompact_InPlaceInitializerList', + 'TestAllocateProxyShared_StrongCompact_Lifetime_Copy', + 'TestAllocateProxyShared_StrongCompact_Lifetime_Move', + 'TestAllocateProxyShared_StrongCompact_Lifetime_WeakAccess', + 'TestMakeProxyShared_SharedCompact_FromValue', + 'TestMakeProxyShared_SharedCompact_InPlace', + 'TestMakeProxyShared_SharedCompact_InPlaceInitializerList', + 'TestMakeProxyShared_SharedCompact_Lifetime_Copy', + 'TestMakeProxyShared_SharedCompact_Lifetime_Move', + 'TestMakeProxyShared_StrongCompact_FromValue', + 'TestMakeProxyShared_StrongCompact_InPlace', + 'TestMakeProxyShared_StrongCompact_InPlaceInitializerList', + 'TestMakeProxyShared_StrongCompact_Lifetime_Copy', + 'TestMakeProxyShared_StrongCompact_Lifetime_Move', + 'TestMakeProxyShared_StrongCompact_Lifetime_WeakAccess', + 'TestMakeProxyShared_StrongCompact_Lifetime_WeakConversion', + 'TestStdWeakPtrCompatibility', + 'TestMakeProxyView', + ], + 'ProxyDispatchTests': [ + 'TestOpPlus', + 'TestOpMinus', + 'TestOpAsterisk', + 'TestOpSlash', + 'TestOpModulo', + 'TestOpIncrement', + 'TestOpDecrement', + 'TestOpEqualTo', + 'TestOpNotEqualTo', + 'TestOpGreaterThan', + 'TestOpLessThan', + 'TestOpGreaterThanOrEqualTo', + 'TestOpLessThanOrEqualTo', + 'TestOpSpaceship', + 'TestOpLogicalNot', + 'TestOpLogicalAnd', + 'TestOpLogicalOr', + 'TestOpTilde', + 'TestOpAmpersand', + 'TestOpPipe', + 'TestOpCaret', + 'TestOpLeftShift', + 'TestOpRightShift', + 'TestOpPlusAssignment', + 'TestOpMinusAssignment', + 'TestOpMultiplicationAssignment', + 'TestOpDivisionAssignment', + 'TestOpModuloAssignment', + 'TestOpBitwiseAndAssignment', + 'TestOpBitwiseOrAssignment', + 'TestOpBitwiseXorAssignment', + 'TestOpLeftShiftAssignment', + 'TestOpRightShiftAssignment', + 'TestOpComma', + 'TestOpPtrToMem', + 'TestOpParentheses', + 'TestOpBrackets_OneDimensional', + 'TestRhsOpPlus', + 'TestRhsOpMinus', + 'TestRhsOpAsterisk', + 'TestRhsOpSlash', + 'TestRhsOpModulo', + 'TestRhsOpEqualTo', + 'TestRhsOpNotEqualTo', + 'TestRhsOpGreaterThan', + 'TestRhsOpLessThan', + 'TestRhsOpGreaterThanOrEqualTo', + 'TestRhsOpLessThanOrEqualTo', + 'TestRhsOpSpaceship', + 'TestRhsOpLogicalAnd', + 'TestRhsOpLogicalOr', + 'TestRhsOpAmpersand', + 'TestRhsOpPipe', + 'TestRhsOpCaret', + 'TestRhsOpLeftShift', + 'TestRhsOpRightShift', + 'TestRhsOpPlusAssignment', + 'TestRhsOpMinusAssignment', + 'TestRhsOpMultiplicationAssignment', + 'TestRhsOpDivisionAssignment', + 'TestRhsOpModuloAssignment', + 'TestRhsOpBitwiseAndAssignment', + 'TestRhsOpBitwiseOrAssignment', + 'TestRhsOpBitwiseXorAssignment', + 'TestRhsOpLeftShiftAssignment', + 'TestRhsOpRightShiftAssignment', + 'TestRhsOpComma', + 'TestRhsOpPtrToMem', + 'TestIndirectConversion', + 'TestDirectConversion', + 'TestImplciitConversion', + 'TestFreeAsMemDispatch', + 'TestSubstitutionDispatch', + ], + 'ProxyFmtFormatTests': ['TestFormat', 'TestWformat'], + 'ProxyFormatTests': ['TestFormat', 'TestWformat'], + 'ProxyIntegrationTests': ['TestDrawable', 'TestLogger'], + 'ProxyInvocationTests': [ + 'TestArgumentForwarding', + 'TestThrow', + 'TestMultipleDispatches_Unique', + 'TestMultipleDispatches_Duplicated', + 'TestRecursiveDefinition', + 'TestOverloadResolution_Member', + 'TestOverloadResolution_Free', + 'TestOverloadResolution_FreeAsMem', + 'TestNoexcept', + 'TestFunctionPointer', + 'TestMemberDispatchDefault', + 'TestFreeDispatchDefault', + 'TestObserverDispatch', + 'TestQualifiedConvention_Member', + 'TestQualifiedConvention_Free', + ], + 'ProxyLifetimeTests': [ + 'TestDefaultConstrction', + 'TestNullConstrction_Nullptr', + 'TestNullConstrction_TypedNullPointer', + 'TestPolyConstrction_FromValue', + 'TestPolyConstrction_FromValue_Exception', + 'TestPolyConstrction_InPlace', + 'TestPolyConstrction_InPlace_Exception', + 'TestPolyConstrction_InPlaceInitializerList', + 'TestPolyConstrction_InPlaceInitializerList_Exception', + 'TestCopyConstrction_FromValue', + 'TestCopyConstrction_FromValue_Exception', + 'TestCopyConstrction_FromNull', + 'TestMoveConstrction_FromValue', + 'TestMoveConstrction_FromValue_Trivial', + 'TestMoveConstrction_FromNull', + 'TestNullAssignment_FromNullptr_ToValue', + 'TestNullAssignment_FromTypedNullPointer_ToValue', + 'TestNullAssignment_ToNull', + 'TestPolyAssignment_ToValue', + 'TestPolyAssignment_ToValue_Exception', + 'TestPolyAssignment_FromValue_ToNull', + 'TestPolyAssignment_FromValue_ToNull_Exception', + 'TestPolyAssignment_InPlace_ToValue', + 'TestPolyAssignment_InPlace_ToValue_Exception', + 'TestPolyAssignment_InPlace_ToNull', + 'TestPolyAssignment_InPlace_ToNull_Exception', + 'TestPolyAssignment_InPlaceInitializerList_ToValue', + 'TestPolyAssignment_InPlaceInitializerList_ToValue_Exception', + 'TestPolyAssignment_InPlaceInitializerList_ToNull', + 'TestPolyAssignment_InPlaceInitializerList_ToNull_Exception', + 'TestCopyAssignment_FromValue_ToValue', + 'TestCopyAssignment_FromValue_ToValue_Exception', + 'TestCopyAssignment_FromValue_ToSelf', + 'TestCopyAssignment_FromValue_ToNull', + 'TestCopyAssignment_FromValue_ToNull_Exception', + 'TestCopyAssignment_FromNull_ToValue', + 'TestCopyAssignment_FromNull_ToSelf', + 'TestCopyAssignment_FromNull_ToNull', + 'TestMoveAssignment_FromValue_ToValue', + 'TestMoveAssignment_FromValue_ToValue_Exception', + 'TestMoveAssignment_FromValue_ToSelf', + 'TestMoveAssignment_FromValue_ToNull', + 'TestMoveAssignment_FromValue_ToNull_Exception', + 'TestMoveAssignment_FromNull_ToValue', + 'TestMoveAssignment_FromNull_ToSelf', + 'TestMoveAssignment_FromNull_ToNull', + 'TestHasValue', + 'TestOperatorBool', + 'TestEqualsToNullptr', + 'TestReset_FromValue', + 'TestReset_FromNull', + 'TestSwap_Value_Value', + 'TestSwap_Value_Self', + 'TestSwap_Value_Null', + 'TestSwap_Null_Value', + 'TestSwap_Null_Self', + 'TestSwap_Null_Null', + 'TestSwap_Trivial', + 'Test_DirectConvension_Lvalue', + 'Test_DirectConvension_Rvalue', + 'Test_DirectConvension_Rvalue_Exception', + 'Test_CopySubstitution_FromValue', + 'Test_CopySubstitution_FromNull', + 'Test_MoveSubstitution_FromValue', + 'Test_MoveSubstitution_FromNull', + ], + 'ProxyReflectionTests': [ + 'TestRtti_RawPtr', + 'TestRtti_FancyPtr', + 'TestTraits_RawPtr', + 'TestTraits_FancyPtr', + ], + 'ProxyRegressionTests': [ + 'TestUnexpectedCompilerWarning', + 'TestProxiableSelfDependency', + ], + 'ProxyRttiTests': [ + 'TestIndirectCast_Ref_Succeed', + 'TestIndirectCast_Ref_Fail', + 'TestIndirectCast_ConstRef_Succeed', + 'TestIndirectCast_ConstRef_Fail', + 'TestIndirectCast_Copy_Succeed', + 'TestIndirectCast_Copy_Fail', + 'TestIndirectCast_Move_Succeed', + 'TestIndirectCast_Move_Fail', + 'TestIndirectCast_Ptr_Succeed', + 'TestIndirectCast_Ptr_Fail', + 'TestIndirectCast_ConstPtr_Succeed', + 'TestIndirectCast_ConstPtr_Fail', + 'TestIndirectTypeid', + 'TestDirectCast_Ref_Succeed', + 'TestDirectCast_Ref_Fail', + 'TestDirectCast_ConstRef_Succeed', + 'TestDirectCast_ConstRef_Fail', + 'TestDirectCast_Copy_Succeed', + 'TestDirectCast_Copy_Fail', + 'TestDirectCast_Move_Succeed', + 'TestDirectCast_Move_Fail', + 'TestDirectCast_Ptr_Succeed', + 'TestDirectCast_Ptr_Fail', + 'TestDirectCast_ConstPtr_Succeed', + 'TestDirectCast_ConstPtr_Fail', + 'TestDirectTypeid', + ], + 'ProxyViewTests': [ + 'TestViewOfNull', + 'TestViewIndependentUse', + 'TestViewOfOwning', + 'TestViewOfNonOwning', + 'TestOverloadShadowing', + 'TestSubstitution_FromNull', + 'TestSubstitution_FromValue', + 'TestFacadeAware', + ], +} +#pragma autogen pop + +foreach suite : tests.keys() + foreach name : tests[suite] + test( + name, + tests_exe, + args: [f'--gtest_filter=@suite@.@name@'], + protocol: 'gtest', + suite: suite, + depends: autogen_tests, + ) + endforeach +endforeach + +freestanding = get_option('tests_freestanding') +freestanding_cflags = ['-ffreestanding', '-fno-exceptions', '-fno-rtti'] +freestanding_ldflags = ['-nodefaultlibs'] + +if cxx.has_multi_arguments( + freestanding_cflags + freestanding_ldflags, + required: freestanding, +) + libc_dep = cxx.find_library( + 'c', + required: freestanding, + ) + freestanding_tests_exe = executable( + 'msft_proxy_freestanding_tests', + files('freestanding/proxy_freestanding_tests.cpp'), + implicit_include_directories: false, + dependencies: [msft_proxy4_dep, libc_dep], + cpp_args: freestanding_cflags, + link_args: get_option('b_sanitize') == '' ? freestanding_ldflags : [], + build_by_default: false, + ) + test( + 'TestFreestanding', + freestanding_tests_exe, + suite: 'ProxyFreestandingSupportTests', + ) +endif + +subdir( + 'modules', + if_found: msft_proxy4_mod, +) diff --git a/tests/modules/meson.build b/tests/modules/meson.build new file mode 100644 index 00000000..c6ffde59 --- /dev/null +++ b/tests/modules/meson.build @@ -0,0 +1,44 @@ +tests_modules_exe = executable( + 'msft_proxy_tests_modules', + files('foo.cpp', 'impl.cpp', 'main.cpp'), + implicit_include_directories: false, + cpp_args: modules_interface_cflags, + dependencies: [msft_proxy4_mod, gtest_dep], + build_by_default: false, +) + +autogen_modules_tests = custom_target( + command: [ + autogen_tests_exe, + custom_target( + command: [ + tests_modules_exe, + '--gtest_list_tests', + '--gtest_output=json:@OUTPUT@', + ], + output: 'tests.json', + ), + meson.current_source_dir() / 'meson.build', + ], + output: 'autogen.stamp', + build_by_default: true, +) + +#pragma autogen push +tests = { + 'ProxyModuleSupportTests': ['TestBasic'], +} +#pragma autogen pop + +foreach suite : tests.keys() + foreach name : tests[suite] + test( + name, + tests_modules_exe, + args: [f'--gtest_filter=@suite@.@name@'], + protocol: 'gtest', + suite: suite, + depends: autogen_modules_tests, + ) + endforeach +endforeach diff --git a/tests/proxy_reflection_tests.cpp b/tests/proxy_reflection_tests.cpp index 2041e9a2..5cc90b40 100644 --- a/tests/proxy_reflection_tests.cpp +++ b/tests/proxy_reflection_tests.cpp @@ -17,7 +17,8 @@ struct TraitsReflector { is_copy_constructible_(std::is_copy_constructible_v), is_nothrow_move_constructible_(std::is_nothrow_move_constructible_v), is_nothrow_destructible_(std::is_nothrow_destructible_v), - is_trivial_(std::is_trivial_v) {} + is_trivial_(std::is_trivially_default_constructible_v && + std::is_trivially_copyable_v) {} template struct accessor { diff --git a/tools/extract_example_code_from_docs.py b/tools/extract_example_code_from_docs.py index 405c1dcf..3e8eaac0 100644 --- a/tools/extract_example_code_from_docs.py +++ b/tools/extract_example_code_from_docs.py @@ -1,42 +1,66 @@ -import os +#!/usr/bin/env python3 +# pyright: strict + import re -import sys +import typing as T +from pathlib import Path + +EXAMPLE_PATTERN = re.compile(r"## Example\r?\n\r?\n```cpp\r?\n(.*?)\r?\n```", re.DOTALL) + -def extract_cpp_code(md_path, cpp_path): - with open(md_path, 'r', encoding='utf-8') as f: +def extract_cpp_code(md_path: Path) -> T.Optional[str]: + with open(md_path, "r", encoding="utf-8") as f: content = f.read() - pattern = r'## Example\r?\n\r?\n```cpp\r?\n(.*?)\r?\n```' - code_blocks = re.findall(pattern, content, re.DOTALL) + code_blocks: list[str] = re.findall(EXAMPLE_PATTERN, content) if len(code_blocks) == 0: return # No match, skip elif len(code_blocks) > 1: - raise ValueError(f"File '{md_path}' contains more than one '## Example' C++ code block.") + msg = f"File '{md_path}' contains more than one '## Example' C++ code block." + raise ValueError(msg) cpp_code = code_blocks[0] - header = f"// This file was auto-generated from:\n// {md_path}\n\n" + header = f""" +// This file was auto-generated from: +// {md_path} + +""".lstrip() + + return header + cpp_code - with open(cpp_path, 'w', encoding='utf-8') as out: - out.write(header) - out.write(cpp_code) -def main(): - if len(sys.argv) != 3: - print("Usage: python extract_example_code_from_docs.py ") - sys.exit(1) +def main() -> None: + import argparse + import os + from dataclasses import dataclass - input_dir = sys.argv[1] - output_dir = sys.argv[2] + @dataclass(frozen=True) + class Args: + input_dir: Path + output_dir: Path + + parser = argparse.ArgumentParser() + _ = parser.add_argument("input_dir", type=Path, help="Path to Markdown documents") + _ = parser.add_argument("output_dir", type=Path, help="Source code output path") + args = parser.parse_args(namespace=Args) + + input_dir = args.input_dir + output_dir = args.output_dir for root, _, files in os.walk(input_dir): for file in files: - if file.endswith('.md'): - md_path = os.path.join(root, file) - rel_path = os.path.relpath(md_path, input_dir) - rel_base = os.path.splitext(rel_path)[0].replace(os.sep, '_') - cpp_path = os.path.join(output_dir, f"example_{rel_base}.cpp") - extract_cpp_code(md_path, cpp_path) - -if __name__ == '__main__': + if file.endswith(".md"): + md_path = Path(root) / file + rel_path = md_path.relative_to(input_dir) + rel_base = "_".join([*rel_path.parent.parts, rel_path.stem]) + cpp_path = output_dir / f"example_{rel_base}.cpp" + cpp_code = extract_cpp_code(md_path) + if cpp_code is None: + continue + with open(cpp_path, "w", encoding="utf-8") as f: + _ = f.write(cpp_code) + + +if __name__ == "__main__": main() diff --git a/tools/meson.build b/tools/meson.build new file mode 100644 index 00000000..87398024 --- /dev/null +++ b/tools/meson.build @@ -0,0 +1,4 @@ +autogen_tests_exe = find_program('./meson_autogen_tests.py') +autogen_doctest_exe = find_program('./meson_autogen_doctest.py') +autogen_benchmarks_exe = find_program('./meson_autogen_benchmarks.py') +extract_doctest_exe = find_program('./meson_extract_doctest.py') diff --git a/tools/meson_autogen_benchmarks.py b/tools/meson_autogen_benchmarks.py new file mode 100644 index 00000000..7029144d --- /dev/null +++ b/tools/meson_autogen_benchmarks.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# pyright: strict + +import re +import sys +from pathlib import Path + +from mesonbuild import mformat + +PATTERN = re.compile(r"(?=[A-Z])") + +benchmarkfile = Path(sys.argv[1]) +mesonfile = Path(sys.argv[2]) + +with benchmarkfile.open(encoding="utf-8") as f: + bench = [line.rstrip() for line in f.readlines()] + +with mesonfile.open(encoding="utf-8") as f: + build = f.readlines() + +begin = -1 +end = -1 + +for i, line in enumerate(build): + if begin == -1 and line.strip() == "#pragma autogen push": + begin = i + 1 + elif end == -1 and line.strip() == "#pragma autogen pop": + end = i + +if begin == -1 or end == -1: + raise ValueError + +benchmarks: "dict[str, list[str]]" = {} + +for name in bench: + suite = "".join(PATTERN.split(name.removeprefix("BM_"))[2:4]) + if suite not in benchmarks: + benchmarks[suite] = [] + else: + benchmarks[suite].append(name) + +config = mformat.get_meson_format([mesonfile]) +formatter = mformat.Formatter(config, False, False) +pretty = formatter.format(f"{benchmarks = }", mesonfile) + +if pretty == "".join(build[begin:end]): + exit() + +with mesonfile.open("w", encoding="utf-8") as f: + f.writelines(build[:begin]) + _ = f.write(pretty) + f.writelines(build[end:]) diff --git a/tools/meson_autogen_doctest.py b/tools/meson_autogen_doctest.py new file mode 100644 index 00000000..f07efcf3 --- /dev/null +++ b/tools/meson_autogen_doctest.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +# pyright: strict + +import sys +from pathlib import Path + +from mesonbuild import mformat + +from extract_example_code_from_docs import extract_cpp_code + +directory = Path(sys.argv[1]) +mesonfile = directory / "meson.build" + +with mesonfile.open(encoding="utf-8") as f: + build = f.readlines() + +begin = -1 +end = -1 + +for i, line in enumerate(build): + if begin == -1 and line.strip() == "#pragma autogen push": + begin = i + 1 + elif end == -1 and line.strip() == "#pragma autogen pop": + end = i + +if begin == -1 or end == -1: + raise ValueError + +docs = [ + str(path.relative_to(directory).as_posix()) + for path in directory.glob("**/*.md") + if extract_cpp_code(path) is not None +] + +config = mformat.get_meson_format([mesonfile]) +formatter = mformat.Formatter(config, False, False) +pretty = formatter.format(f"{docs = }", mesonfile) + +if pretty == "".join(build[begin:end]): + exit() + +with mesonfile.open("w", encoding="utf-8") as f: + f.writelines(build[:begin]) + _ = f.write(pretty) + f.writelines(build[end:]) diff --git a/tools/meson_autogen_tests.py b/tools/meson_autogen_tests.py new file mode 100644 index 00000000..d25e02c2 --- /dev/null +++ b/tools/meson_autogen_tests.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# pyright: strict + +import json +import sys +from pathlib import Path + +from mesonbuild import mformat + +testfile = Path(sys.argv[1]) +mesonfile = Path(sys.argv[2]) + +with testfile.open(encoding="utf-8") as f: + tests = { + suites["name"]: [suite["name"] for suite in suites["testsuite"]] + for suites in json.load(f)["testsuites"] + } + +with mesonfile.open(encoding="utf-8") as f: + build = f.readlines() + +begin = -1 +end = -1 + +for i, line in enumerate(build): + if begin == -1 and line.strip() == "#pragma autogen push": + begin = i + 1 + elif end == -1 and line.strip() == "#pragma autogen pop": + end = i + +if begin == -1 or end == -1: + raise ValueError + +config = mformat.get_meson_format([mesonfile]) +formatter = mformat.Formatter(config, False, False) +pretty = formatter.format(f"{tests = }", mesonfile) + +if pretty == "".join(build[begin:end]): + exit() + +with mesonfile.open("w", encoding="utf-8") as f: + f.writelines(build[:begin]) + _ = f.write(pretty) + f.writelines(build[end:]) diff --git a/tools/meson_extract_doctest.py b/tools/meson_extract_doctest.py new file mode 100644 index 00000000..243ab654 --- /dev/null +++ b/tools/meson_extract_doctest.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# pyright: strict + +import sys +from pathlib import Path + +from extract_example_code_from_docs import extract_cpp_code + +infile = Path(sys.argv[1]) +outfile = Path(sys.argv[2]) + +cpp_code = extract_cpp_code(infile) + +if not cpp_code: + raise ValueError + +with open(outfile, "w") as f: + _ = f.write(cpp_code)