diff --git a/CMakeLists.txt b/CMakeLists.txt index c779136..5c9a01d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -70,3 +70,4 @@ set_property(GLOBAL PROPERTY USE_FOLDERS ON) mwg_add_runtime_data_target() add_subdirectory(src/MapWinGIS.Core) +add_subdirectory(src/MapWinGIS.ActiveX) diff --git a/build/scripts/Compare-CompileCommands.ps1 b/build/scripts/Compare-CompileCommands.ps1 index 006051a..3b3cfbf 100644 --- a/build/scripts/Compare-CompileCommands.ps1 +++ b/build/scripts/Compare-CompileCommands.ps1 @@ -83,6 +83,17 @@ $explainedLinkDifferences = [ordered]@{ '^lib:/' = 'Library from an imported target (find_package)' '^lib:odbc(cp)?32\.lib$' = 'MSBuild default library; unused, not in CMake''s default list' '^/version:0\.0$' = 'CMake default image version; MSBuild leaves it at 0.0 too' + '^input:.*\.res$' = 'Compiled resource file name (.rc.res in CMake)' + '^lib:(/)?lib[\\/]mapwingis\.core\.lib$' = 'Core import library from target_link_libraries (MSBuild: project reference)' +} + +# Compile differences that are deliberate (docs/CMAKE-MIGRATION.md). +$explainedCompileDifferences = [ordered]@{ + '^/I$' = 'MIDL output moved from the intermediate directory to /.../midl (D3)' + '^/I/.*/midl$' = 'MIDL output directory (D3)' + '^/I/[^/]+/include$' = 'vcpkg auto-include; targets without a vcpkg dependency do not get it, the others get it from imported targets' + '^/I/[^/]+/include/geos$' = 'Interface include directory of GEOS::geos_c' + '^/DGDAL_DEBUG$' = 'Interface definition of the debug GDAL::GDAL' } # MSBuild project names that differ from the CMake target names. @@ -290,7 +301,7 @@ function Read-MSBuildCommands([string] $logPath, [string] $binDir) { $set = ConvertTo-OptionSet $arguments $baseDir $placeholders @('.c', '.cpp', '.cxx', '.cc') foreach ($source in $set.Sources) { # Keyed by target: the tests compile some ActiveX sources a second time. - $compiles["$target $source"] = [pscustomobject]@{ + $compiles[(Get-SourceKey $target $source)] = [pscustomobject]@{ Target = $target Source = $source Options = Merge-VcpkgPlatform $set.Options @@ -327,6 +338,8 @@ function Read-CMakeCommands([string] $buildDir, [string] $logPath, [string] $bin } foreach ($entry in (Get-Content -Raw -LiteralPath $database | ConvertFrom-Json)) { + # Resource compiles are in the database too; the MSBuild side has only cl.exe. + if ($entry.file -match '\.rc$') { continue } $arguments = if ($entry.arguments) { @($entry.arguments | Select-Object -Skip 1) } else { @(Split-CommandLine $entry.command | Select-Object -Skip 1) } $fo = $arguments | Where-Object { $_ -match '^[/-]Fo' } | Select-Object -First 1 @@ -335,7 +348,7 @@ function Read-CMakeCommands([string] $buildDir, [string] $logPath, [string] $bin $set = ConvertTo-OptionSet $arguments $entry.directory $placeholders @('.c', '.cpp', '.cxx', '.cc') $source = Get-NormalizedPath $entry.file $entry.directory $placeholders $target = if ($intDir -match '\\CMakeFiles\\(?[^\\]+)\.dir') { $Matches['t'] } else { '' } - $compiles["$target $source"] = [pscustomobject]@{ + $compiles[(Get-SourceKey $target $source)] = [pscustomobject]@{ Target = $target Source = $source Options = Merge-VcpkgPlatform $set.Options @@ -407,6 +420,31 @@ function Compare-OptionSets($reference, $actual) { return [pscustomobject]@{ Lost = $lost; Added = $added } } +# Moves the differences that match a pattern into 'Explained', with the reason. +function Split-ExplainedDifferences($diff, $patterns) { + $explained = [ordered]@{} + $lost = @(); $added = @() + foreach ($pair in @(@{ Items = $diff.Lost; Kind = 'lost' }, @{ Items = $diff.Added; Kind = 'added' })) { + foreach ($option in $pair.Items) { + $reason = $null + foreach ($pattern in $patterns.Keys) { + if ($option -match $pattern) { $reason = $patterns[$pattern]; break } + } + if ($reason) { $explained["$($pair.Kind) $option"] = $reason } + elseif ($pair.Kind -eq 'lost') { $lost += $option } + else { $added += $option } + } + } + return [pscustomobject]@{ Lost = $lost; Added = $added; Explained = $explained } +} + +# MIDL output lives in different directories in the two builds (D3); key the +# generated sources by file name. +function Get-SourceKey([string] $target, [string] $source) { + if ($source -match '^<(intdir|builddir)>/(?:.*/)?(?[^/]+_i\.c)$') { $source = "/$($Matches['file'])" } + return "$target $source" +} + # --- MSBuild baseline ------------------------------------------------------ if (-not $MSBuildBinDirectory) { @@ -462,8 +500,9 @@ if ($CMakeBuildDir) { } $diff = Compare-OptionSets $reference.Options $actual.Options if ($diff.Lost.Count -or $diff.Added.Count) { - $compileDiff[$key] = $diff - $lostTotal += $diff.Lost.Count + $split = Split-ExplainedDifferences $diff $explainedCompileDifferences + $compileDiff[$key] = $split + $lostTotal += $split.Lost.Count } } foreach ($key in ($cmakeCommands.Compiles.Keys | Where-Object { -not $msbuildCommands.Compiles.ContainsKey($_) })) { @@ -482,22 +521,10 @@ if ($CMakeBuildDir) { continue } $diff = Compare-OptionSets $msbuildCommands.Links[$binary] $cmakeCommands.Links[$binary] - $explained = [ordered]@{} - $lost = @(); $added = @() - foreach ($pair in @(@{ Items = $diff.Lost; Kind = 'lost' }, @{ Items = $diff.Added; Kind = 'added' })) { - foreach ($option in $pair.Items) { - $reason = $null - foreach ($pattern in $explainedLinkDifferences.Keys) { - if ($option -match $pattern) { $reason = $explainedLinkDifferences[$pattern]; break } - } - if ($reason) { $explained["$($pair.Kind) $option"] = $reason } - elseif ($pair.Kind -eq 'lost') { $lost += $option } - else { $added += $option } - } - } if ($diff.Lost.Count -or $diff.Added.Count) { - $linkDiff[$binary] = [pscustomobject]@{ Lost = $lost; Added = $added; Explained = $explained } - $lostTotal += $lost.Count + $split = Split-ExplainedDifferences $diff $explainedLinkDifferences + $linkDiff[$binary] = $split + $lostTotal += $split.Lost.Count } } diff --git a/cmake/MwgCompilerOptions.cmake b/cmake/MwgCompilerOptions.cmake index c54ef4d..95c1c97 100644 --- a/cmake/MwgCompilerOptions.cmake +++ b/cmake/MwgCompilerOptions.cmake @@ -41,6 +41,8 @@ function(mwg_apply_common_options target) "$<${c_cxx}:/utf-8;/Zc:__cplusplus;/Zc:preprocessor;/Zc:throwingNew;/bigobj>" # ConformanceMode, SDLCheck, ExceptionHandling and MSBuild defaults "$<${c_cxx}:/permissive-;/sdl;/EHsc;/GS;/Gd;/Gm-;/fp:precise;/Zc:wchar_t;/Zc:forScope;/Zc:inline>" + # CMake passes /TP for C++ but not /TC for C (the MIDL _i.c). + "$<$:/TC>" "$<${debug}:/Od;/JMC>" "$<${release}:/O2;/Oi;/Gy;/guard:cf>" ) diff --git a/cmake/MwgMidl.cmake b/cmake/MwgMidl.cmake new file mode 100644 index 0000000..f9cf2b9 --- /dev/null +++ b/cmake/MwgMidl.cmake @@ -0,0 +1,111 @@ +# SPDX-License-Identifier: MPL-2.0 +# +# mwg_add_midl_target( IDL [OUTPUT_DIR ] [DEPENDS ...]) +# +# Runs midl.exe through one explicit custom command for every generator (only +# the Visual Studio generators understand .idl sources), see +# docs/CMAKE-MIGRATION.md (D3). Creates: +# +# custom target that owns the MIDL invocation +# .Generated INTERFACE library: the MIDL output include directory, +# for the OCX and the tests +# +# and sets, in the caller's scope: +# _HEADER _i.h +# _IID _i.c (C, compile with the consumer) +# _TLB .tlb +# _DIR output directory +# +# The options match the MSBuild Midl task: /W1 /char signed /env x64|win32 +# /target "NT60". This IDL declares no non-automation interfaces, so MIDL +# writes no proxy or dlldata file; they are redirected into the output +# directory anyway and are not declared as outputs. + +function(_mwg_find_midl out_var) + # The MSBuild Midl task uses the SDK's x86 midl.exe; any host build works. + set(hints "") + if(CMAKE_RC_COMPILER) + get_filename_component(rc_dir "${CMAKE_RC_COMPILER}" DIRECTORY) + get_filename_component(sdk_bin "${rc_dir}" DIRECTORY) + list(APPEND hints "${rc_dir}" "${sdk_bin}/x64" "${sdk_bin}/x86") + endif() + find_program(MWG_MIDL_EXECUTABLE midl HINTS ${hints} DOC "MIDL compiler from the Windows SDK") + if(NOT MWG_MIDL_EXECUTABLE) + message(FATAL_ERROR + "midl.exe was not found. It ships with the Windows SDK ${MWG_WINDOWS_SDK}; " + "configure from a Visual Studio toolchain (CLion) or a VS 2026 Developer PowerShell.") + endif() + set(${out_var} "${MWG_MIDL_EXECUTABLE}" PARENT_SCOPE) +endfunction() + +function(mwg_add_midl_target target) + cmake_parse_arguments(PARSE_ARGV 1 arg "" "IDL;OUTPUT_DIR" "DEPENDS") + if(NOT arg_IDL) + message(FATAL_ERROR "mwg_add_midl_target: IDL is required") + endif() + if(NOT arg_OUTPUT_DIR) + set(arg_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/midl") + endif() + + _mwg_find_midl(midl) + + # MIDL preprocesses the IDL with cl.exe, which it finds on PATH only. The + # Ninja generator inherits the vcvars PATH; the Visual Studio generators + # run custom commands in an MSBuild environment that has it. A plain shell + # with a cl.exe from CMAKE_C_COMPILER is not enough. + if(NOT CMAKE_GENERATOR MATCHES "^Visual Studio") + find_program(_mwg_cl_on_path cl NO_CACHE NO_DEFAULT_PATH PATHS ENV PATH) + if(NOT _mwg_cl_on_path) + message(FATAL_ERROR + "cl.exe is not on PATH, but midl.exe needs it to preprocess ${arg_IDL}.\n" + "Use a Visual Studio toolchain in CLion, or configure and build from a " + "VS 2026 Developer PowerShell (vcvarsall.bat).") + endif() + endif() + + cmake_path(ABSOLUTE_PATH arg_IDL BASE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" OUTPUT_VARIABLE idl) + # LAST_ONLY: MapWinGIS.ActiveX.idl -> MapWinGIS.ActiveX + cmake_path(GET idl STEM LAST_ONLY name) + cmake_path(GET idl PARENT_PATH idl_dir) + + if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(env x64) + else() + set(env win32) + endif() + + set(header "${arg_OUTPUT_DIR}/${name}_i.h") + set(iid "${arg_OUTPUT_DIR}/${name}_i.c") + set(tlb "${arg_OUTPUT_DIR}/${name}.tlb") + + add_custom_command( + OUTPUT "${header}" "${iid}" "${tlb}" + COMMAND "${CMAKE_COMMAND}" -E make_directory "${arg_OUTPUT_DIR}" + COMMAND "${midl}" /W1 /nologo /char signed /env ${env} /target NT60 + /I "${idl_dir}" + /out "${arg_OUTPUT_DIR}" + /h "${name}_i.h" + /iid "${name}_i.c" + /proxy "${name}_p.c" + /dlldata dlldata.c + /tlb "${name}.tlb" + "${idl}" + MAIN_DEPENDENCY "${idl}" + DEPENDS ${arg_DEPENDS} + WORKING_DIRECTORY "${idl_dir}" + COMMENT "MIDL ${name}.idl" + VERBATIM + ) + + add_custom_target(${target} DEPENDS "${header}" "${iid}" "${tlb}" SOURCES "${idl}") + set_target_properties(${target} PROPERTIES FOLDER "src") + + add_library(${target}.Generated INTERFACE) + target_include_directories(${target}.Generated INTERFACE "${arg_OUTPUT_DIR}") + add_dependencies(${target}.Generated ${target}) + + set(${target}_HEADER "${header}" PARENT_SCOPE) + set(${target}_IID "${iid}" PARENT_SCOPE) + set(${target}_TLB "${tlb}" PARENT_SCOPE) + set(${target}_DIR "${arg_OUTPUT_DIR}" PARENT_SCOPE) +endfunction() diff --git a/docs/BUILDING.md b/docs/BUILDING.md index d8315d0..2547947 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -198,8 +198,11 @@ MSBuild settings and the decisions the CMake build follows are in ### Building with CMake -The CMake build currently covers `MapWinGIS.Core.dll`; the ActiveX control, -tests and samples follow. It needs CMake 3.28 or later and Ninja; both ship +The CMake build currently covers `MapWinGIS.Core.dll` and `MapWinGIS.ocx`; the +tests and samples follow. The OCX needs the **C++ ATL for latest v145 build +tools** component for each architecture you build, and `midl.exe` needs `cl.exe` +on `PATH`, so configure and build from a Visual Studio toolchain (CLion) or a +Developer PowerShell. CMake 3.28 or later and Ninja are required; both ship with Visual Studio 2026 and CLion. Run the bootstrap script first, then configure from a VS 2026 Developer PowerShell (or any shell after `vcvarsall.bat x64`): @@ -218,7 +221,11 @@ cmake --build --preset x64-debug - Output goes to `\bin` (`bin\` for the Visual Studio presets) and import libraries to `lib`, never to the MSBuild `bin\` folder, so - both builds can be used side by side. + both builds can be used side by side. MIDL output (`MapWinGIS.ActiveX_i.h`, + `_i.c`, `.tlb`) goes to `\src\MapWinGIS.ActiveX\midl`. +- To register the CMake-built control for the current user: + `regsvr32 /n /i:user out\b\x64d\bin\MapWinGIS.ocx` (use + `%WINDIR%\SysWOW64\regsvr32.exe` for Win32; add `/u` to unregister). - The vcpkg root is found in the same order as above. CMake uses its own install tree, `vcpkg_installed\cmake-`, shared by Debug and Release; the binary cache makes it cheap. diff --git a/docs/CMAKE-MIGRATION.md b/docs/CMAKE-MIGRATION.md index 7cf1e68..779a700 100644 --- a/docs/CMAKE-MIGRATION.md +++ b/docs/CMAKE-MIGRATION.md @@ -328,3 +328,31 @@ Measured for Debug and Release, x64 and Win32 (Ninja presets): by default for that platform. - `dumpbin`: identical dependents, exports (260), subsystem and DLL characteristics. + +### MapWinGIS.ActiveX (Phase 2) + +Measured for Debug and Release, x64 and Win32 (Ninja presets): + +- Compile and link options: none lost; 0 warnings. Explained differences: + - The MIDL include directory is `/src/MapWinGIS.ActiveX/midl` + instead of the intermediate directory (D3); the generated `_i.c` is + compared by file name. + - No vcpkg include directory: the OCX has no direct vcpkg dependency. + - The compiled resource is named `MapWinGIS.ActiveX.rc.res`, and the Core + import library comes from `target_link_libraries` instead of the project + reference. +- CMake needs two explicit settings to match MSBuild: `DEFINE_SYMBOL ""` (no + `MapWinGIS_ActiveX_EXPORTS`; the `.def` file exports the entry points) and + `/TC` for C sources (CMake passes `/TP` for C++ only). +- `dumpbin`: identical dependents, the five COM exports, subsystem and DLL + characteristics. +- The generated type library is byte-identical to the MSBuild one, and + `MapWinGIS.ActiveX_i.h` is identical below its generated header comment. +- MIDL runs through one custom command for all generators + (`cmake/MwgMidl.cmake`) with the MSBuild options. This IDL produces no + proxy or `dlldata.c`; MIDL is told to write them into the output directory + anyway, so nothing can land in the source tree. +- Per-user registration (`regsvr32 /n /i:user`), `New-Object -ComObject + MapWinGIS6.Control` and unregistration work for the x64 and Win32 OCX. +- Three clean `-j32` Ninja builds in a row succeeded (no MIDL/RC race), and the + Visual Studio generator builds x64 Debug/Release and x86 Debug. diff --git a/src/MapWinGIS.ActiveX/CMakeLists.txt b/src/MapWinGIS.ActiveX/CMakeLists.txt new file mode 100644 index 0000000..211b534 --- /dev/null +++ b/src/MapWinGIS.ActiveX/CMakeLists.txt @@ -0,0 +1,99 @@ +# SPDX-License-Identifier: MPL-2.0 +# +# MapWinGIS.ocx: thin ActiveX/COM wrapper over MapWinGIS.Core (static ATL, +# apartment threaded). + +include(MwgMidl) + +mwg_add_midl_target(MapWinGIS.ActiveX.Interface IDL MapWinGIS.ActiveX.idl) + +# Compiled into the OCX and, with MWG_ENABLE_RENDERER_TEST_HOOKS, into the +# tests (docs/CMAKE-MIGRATION.md, D4). +set(MWG_ACTIVEX_SHARED_SOURCES + ComConversion.cpp + GdalOperationDispatcher.cpp + GdiVectorLayerRenderer.cpp + OpenGlVectorLayerRenderer.cpp + VectorLayerRenderer.cpp + ViewTransform.cpp +) +list(TRANSFORM MWG_ACTIVEX_SHARED_SOURCES PREPEND "${CMAKE_CURRENT_SOURCE_DIR}/" + OUTPUT_VARIABLE MWG_ACTIVEX_SHARED_SOURCES) +set(MWG_ACTIVEX_SHARED_SOURCES "${MWG_ACTIVEX_SHARED_SOURCES}" PARENT_SCOPE) + +set(MWG_ACTIVEX_HEADERS + ActiveXModule.h + ComConversion.h + GdalDispatchWindow.h + GdalEventSource.h + GdalOperationDispatcher.h + GdiVectorLayerRenderer.h + MapWinGISControl.h + OpenGlVectorLayerRenderer.h + VectorLayerRenderer.h + VectorRenderBackend.h + ViewTransform.h + resource.h +) + +set(MWG_ACTIVEX_SOURCES + ActiveXModule.cpp + GdalEventSource.cpp + MapWinGISControl.cpp +) + +set(MWG_ACTIVEX_RESOURCES + MapWinGIS.ActiveX.rc + MapWinGIS.ActiveX.def + MapWinGISControl.rgs +) + +# SHARED, not MODULE: keeps the import library and the runtime-DLL handling. +add_library(MapWinGIS.ActiveX SHARED) + +target_sources(MapWinGIS.ActiveX PRIVATE + ${MWG_ACTIVEX_HEADERS} + ${MWG_ACTIVEX_SOURCES} + ${MWG_ACTIVEX_SHARED_SOURCES} + ${MWG_ACTIVEX_RESOURCES} + "${MapWinGIS.ActiveX.Interface_HEADER}" + "${MapWinGIS.ActiveX.Interface_IID}" +) +source_group("Header Files" FILES ${MWG_ACTIVEX_HEADERS}) +source_group("Source Files" FILES ${MWG_ACTIVEX_SOURCES} ${MWG_ACTIVEX_SHARED_SOURCES}) +source_group("Resource Files" FILES ${MWG_ACTIVEX_RESOURCES}) +source_group("Generated Files" FILES + "${MapWinGIS.ActiveX.Interface_HEADER}" + "${MapWinGIS.ActiveX.Interface_IID}") + +# The .rc embeds the generated type library (1 TYPELIB "MapWinGIS.ActiveX.tlb"); +# rc's dependency scanner does not see it. +set_source_files_properties(MapWinGIS.ActiveX.rc PROPERTIES + OBJECT_DEPENDS "${MapWinGIS.ActiveX.Interface_TLB};${CMAKE_CURRENT_SOURCE_DIR}/MapWinGISControl.rgs") + +target_link_libraries(MapWinGIS.ActiveX PRIVATE + MapWinGIS::Core + MapWinGIS.ActiveX.Interface.Generated + # Explicit Windows libraries (audit). ATL is static: atlbase.h links + # atls.lib through #pragma comment, and OpenGlVectorLayerRenderer.cpp links + # opengl32.lib the same way. + ole32 oleaut32 uuid user32 gdi32 advapi32 kernel32 +) + +target_compile_definitions(MapWinGIS.ActiveX PRIVATE "$<$:_ATL_APARTMENT_THREADED>") + +# MSBuild ResourceCompile: /l 0x0409 and the MIDL output directory on the include path. +target_compile_options(MapWinGIS.ActiveX PRIVATE "$<$:/l0x0409>") + +mwg_apply_common_options(MapWinGIS.ActiveX) + +set_target_properties(MapWinGIS.ActiveX PROPERTIES + OUTPUT_NAME MapWinGIS + SUFFIX ".ocx" + # The COM entry points are exported through MapWinGIS.ActiveX.def; no + # _EXPORTS macro, as in MSBuild. + DEFINE_SYMBOL "" + FOLDER "src" +) + +add_dependencies(MapWinGIS.ActiveX MapWinGIS.ActiveX.Interface)