Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
65 changes: 46 additions & 19 deletions build/scripts/Compare-CompileCommands.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,17 @@ $explainedLinkDifferences = [ordered]@{
'^lib:<vcpkg_installed>/' = '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 (<name>.rc.res in CMake)'
'^lib:(<builddir>/)?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<intdir>$' = 'MIDL output moved from the intermediate directory to <build>/.../midl (D3)'
'^/I<builddir>/.*/midl$' = 'MIDL output directory (D3)'
'^/I<vcpkg_installed>/[^/]+/include$' = 'vcpkg auto-include; targets without a vcpkg dependency do not get it, the others get it from imported targets'
'^/I<vcpkg_installed>/[^/]+/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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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\\(?<t>[^\\]+)\.dir') { $Matches['t'] } else { '' }
$compiles["$target $source"] = [pscustomobject]@{
$compiles[(Get-SourceKey $target $source)] = [pscustomobject]@{
Target = $target
Source = $source
Options = Merge-VcpkgPlatform $set.Options
Expand Down Expand Up @@ -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)>/(?:.*/)?(?<file>[^/]+_i\.c)$') { $source = "<generated>/$($Matches['file'])" }
return "$target $source"
}

# --- MSBuild baseline ------------------------------------------------------

if (-not $MSBuildBinDirectory) {
Expand Down Expand Up @@ -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($_) })) {
Expand All @@ -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
}
}

Expand Down
2 changes: 2 additions & 0 deletions cmake/MwgCompilerOptions.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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).
"$<$<COMPILE_LANGUAGE:C>:/TC>"
"$<${debug}:/Od;/JMC>"
"$<${release}:/O2;/Oi;/Gy;/guard:cf>"
)
Expand Down
111 changes: 111 additions & 0 deletions cmake/MwgMidl.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# SPDX-License-Identifier: MPL-2.0
#
# mwg_add_midl_target(<target> IDL <file.idl> [OUTPUT_DIR <dir>] [DEPENDS <file>...])
#
# 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:
#
# <target> custom target that owns the MIDL invocation
# <target>.Generated INTERFACE library: the MIDL output include directory,
# for the OCX and the tests
#
# and sets, in the caller's scope:
# <target>_HEADER <name>_i.h
# <target>_IID <name>_i.c (C, compile with the consumer)
# <target>_TLB <name>.tlb
# <target>_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()
13 changes: 10 additions & 3 deletions docs/BUILDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`):
Expand All @@ -218,7 +221,11 @@ cmake --build --preset x64-debug

- Output goes to `<build directory>\bin` (`bin\<Config>` 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 `<build directory>\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-<Platform>`, shared by Debug and Release; the
binary cache makes it cheap.
Expand Down
28 changes: 28 additions & 0 deletions docs/CMAKE-MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<build>/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.
99 changes: 99 additions & 0 deletions src/MapWinGIS.ActiveX/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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 "$<$<COMPILE_LANGUAGE:C,CXX>:_ATL_APARTMENT_THREADED>")

# MSBuild ResourceCompile: /l 0x0409 and the MIDL output directory on the include path.
target_compile_options(MapWinGIS.ActiveX PRIVATE "$<$<COMPILE_LANGUAGE:RC>:/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
# <target>_EXPORTS macro, as in MSBuild.
DEFINE_SYMBOL ""
FOLDER "src"
)

add_dependencies(MapWinGIS.ActiveX MapWinGIS.ActiveX.Interface)