Adding custom classes to root with Visual Studio

Hi,

a couple of years ago I programmed powerfull classes in a unix enviroment, I had a running build system which directly generated all necessarry libraries to be able to directly use these classes in root.

Nowerdays I’m limited to a Windows system and I would like to recover some of these old classes, because of this I’m asking:

“Is there a documentation somewhere describing what steps need to be done to build classes in Visual Studio in order to make these classes available in the root environment?”

I know about all the #pragma stuff, but what I’m search for is a minimal Visual Studio project containing an example class.

Alternatively: Some minimal code and the necessary build commands and the commands to generate the dictionaries

Thanks

Georg


ROOT Version: 6.28.06
Platform: Windows
Compiler: Visual Studio 2022 (17.3.3)


Well, you can use the same procedure than on Linux, with CMake. I you have a concrete use case I can help you fixing the issue you might have on Windows.

@bellenot,

Thank you for your help. Let me try it a bit more specific:

I created a simple test class inside a DLL-template of visual studio:

#pragma once
class gttest
{
public:
gttest() {}
virtual ~gttest() {}
int run() { return 42; }
};

#ifdef __CLING__
#pragma link C++ class gttest+;
#endif // __CLING__

it compiles and creates a dll-file.

Then manually I ran:

rootcling-exe -f DictOutput.cxx gttest.h

which again creates me DictOutput.cxx and DictOutput_rdict.pcm (I’m sure I can add this to the Dll-template, so it runs automatically)

In my old unix environment I had to do some path-magic after that (adding to LD_LIBRARY_PATH) to let root know where to find something.

Here I tried to copy everything into one folder, as I do not know how to handle the path magic in windows, unfortunately root still doesn’t know anything about “gttest”.

I also tried to do inside root

gSystem->Load("<dll-file>")

… it returns 0 and does nothing

Any hint what I have to do?

Georg

Well, if gSystem->Load("<dll-file>") returns 0, that means it was successfully loaded. What else do you think it’s supposed to do?

@Bellenot:

See here:

PS C:\Users\troska\source\repos\root20260116\x64\Debug> ls

    Directory: C:\Users\troska\source\repos\root20260116\x64\Debug

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---          16.01.2026    12:22           1050 DictOutput_rdict.pcm
-a---          16.01.2026    12:22           5420 DictOutput.cxx
-a---          16.01.2026    12:18            186 gttest.h
-a---          16.01.2026    12:19          58880 root20260116.dll
-a---          16.01.2026    12:19        1396736 root20260116.pdb

PS C:\Users\troska\source\repos\root20260116\x64\Debug> root -l
root [0] gSystem->Load("root20260116.dll")
(int) 0
root [1] gttest *g = new gttest()
ROOT_prompt_1:1:17: error: unknown type name 'gttest'
gttest *g = new gttest()
                ^
root [2]

seems root doesn’t know anything about this class

Georg

OK, thanks. I’ll give it a try and let you know asap

thank you!

So how do you build your DLL? How do you specify which symbols to export?

@Bellenot:

Here are the compiler options

/JMC /permissive- /Yu"pch.h" /ifcOutput “x64\Debug" /GS /W3 /Zc:wchar_t /ZI /Gm- /Od /sdl /Fd"x64\Debug\vc143.pdb” /Zc:inline /fp:precise /D “_DEBUG” /D “ROOT20260116_EXPORTS” /D “_WINDOWS” /D “_USRDLL” /D “_WINDLL” /D “_UNICODE” /D “UNICODE” /errorReport:prompt /WX- /Zc:forScope /RTC1 /Gd /MDd /FC /Fa"x64\Debug" /EHsc /nologo /Fo"x64\Debug" /Fp"x64\Debug\root20260116.pch" /diagnostics:column

and the linker options

/OUT:“C:\Users\troska\source\repos\root20260116\x64\Debug\root20260116.dll” /MANIFEST /NXCOMPAT /PDB:“C:\Users\troska\source\repos\root20260116\x64\Debug\root20260116.pdb” /DYNAMICBASE “kernel32.lib” “user32.lib” “gdi32.lib” “winspool.lib” “comdlg32.lib” “advapi32.lib” “shell32.lib” “ole32.lib” “oleaut32.lib” “uuid.lib” “odbc32.lib” “odbccp32.lib” /IMPLIB:“C:\Users\troska\source\repos\root20260116\x64\Debug\root20260116.lib” /DEBUG /DLL /MACHINE:X64 /INCREMENTAL /PGD:“C:\Users\troska\source\repos\root20260116\x64\Debug\root20260116.pgd” /SUBSYSTEM:WINDOWS /MANIFESTUAC:NO /ManifestFile:“x64\Debug\root20260116.dll.intermediate.manifest” /LTCGOUT:“x64\Debug\root20260116.iobj” /ERRORREPORT:PROMPT /ILK:“x64\Debug\root20260116.ilk” /NOLOGO /TLBID:1

I didn’t change anything here, its just the standard Visual Studio template

So you DLL doesn’t export any symbol, meaning nothing can be called from outside. Just try:

dumpbin /exports root20260116.dll

@bellenot:

you are right:

PS C:\Users\troska\source\repos\root20260116\x64\Debug> .\dumpbin.exe /exports .\root20260116.dll
Microsoft (R) COFF/PE Dumper Version 14.44.35222.0
Copyright (C) Microsoft Corporation.  All rights reserved.

Dump of file .\root20260116.dll

File Type: DLL

Summary

    1000 .00cfg
    1000 .data
    1000 .idata
    1000 .msvcjmc
    3000 .pdata
    3000 .rdata
    1000 .reloc
    1000 .rsrc
    8000 .text
   10000 .textbss

PS C:\Users\troska\source\repos\root20260116\x64\Debug>

nothing inside….I hate Visual Studio so much (thinks are som much more transparent with Makefiles)… what is necessary to export all symbols inside this Dll-project?

@Bellenot:

I now changed the class definition to

#pragma once
class __declspec(dllexport) gttest
{
public:
gttest() {}
virtual ~gttest() {}
int run() { return 42; }
};

#ifdef __CLING__
#pragma link C++ class gttest+;
#endif // __CLING__

due to this there is something inside now:

PS C:\Users\troska\source\repos\root20260116\x64\Debug> .\dumpbin.exe /exports .\root20260116.dll
Microsoft (R) COFF/PE Dumper Version 14.44.35222.0
Copyright (C) Microsoft Corporation.  All rights reserved.

Dump of file .\root20260116.dll

File Type: DLL

Section contains the following exports for root20260116.dll

00000000 characteristics
FFFFFFFF time date stamp
    0.00 version
       1 ordinal base
       6 number of functions
       6 number of names

ordinal hint RVA      name

      1    0 0001134D ??0gttest@@QEAA@AEBV0@@Z = @ILT+840(??0gttest@@QEAA@AEBV0@@Z)
      2    1 000112EE ??0gttest@@QEAA@XZ = @ILT+745(??0gttest@@QEAA@XZ)
      3    2 000111C7 ??1gttest@@UEAA@XZ = @ILT+450(??1gttest@@UEAA@XZ)
      4    3 00011104 ??4gttest@@QEAAAEAV0@AEBV0@@Z = @ILT+255(??4gttest@@QEAAAEAV0@AEBV0@@Z)
      5    4 0001A888 ??_7gttest@@6B@ = ??_7gttest@@6B@ (const gttest::`vftable')
      6    5 000111B8 ?run@gttest@@QEAAHXZ = @ILT+435(?run@gttest@@QEAAHXZ)

Summary

    1000 .00cfg
    1000 .data
    1000 .idata
    1000 .msvcjmc
    3000 .pdata
    3000 .rdata
    1000 .reloc
    1000 .rsrc
    9000 .text
   10000 .textbss

PS C:\Users\troska\source\repos\root20260116\x64\Debug>

great!

I reran rootcling.exe

but unfortunately:

root [0] gSystem->Load(“root20260116”)
(int) 0
root [1] gttest
input_line_11:2:3: error: use of undeclared identifier ‘gttest’
(gttest)
^
root [2]

sorry for this __declspec -stuff….

So here is my proposed solution, using CMake, with the attached CMakeLists.txt. Add it in your C:\Users\troska\source\repos\root20260116 directory, and create a build subdirectory.
Then open a Developer Command Prompt for VS 2022 and go to C:\Users\troska\source\repos\root20260116\build. Then, assuming ROOTSYS is properly set, type:

cmake -Wno-dev -G"Visual Studio 17 2022" -A x64 -Thost=x64 -DCMAKE_VERBOSE_MAKEFILE=ON ..\

The output should look like:

C:\Users\troska\source\repos\root20260116\build>cmake -Wno-dev -G"Visual Studio 17 2022" -A x64 -Thost=x64 -DCMAKE_VERBOSE_MAKEFILE=ON ..\
-- Selecting Windows SDK version 10.0.26100.0 to target Windows 10.0.26200.
-- The CXX compiler identification is MSVC 19.44.35222.0
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.44.35207/bin/Hostx64/x64/cl.exe - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done (1.7s)
-- Generating done (0.0s)
-- Build files have been written to: C:\Users\troska\source\repos\root20260116\build

Then build the DLL:

C:\Users\troska\source\repos\root20260116\build>cmake --build . --config Release

At the end, if the build is successful, try again dumpbin /exports Release\root20260116.dll, to see the difference…
And finally, you can try with ROOT:

C:\Users\troska\source\repos\root20260116\build>root -l
root [0] gSystem->Load("root20260116.dll")
(int) 0
root [1] gttest *g = new gttest()
(gttest *) 0x1f8b573c890
root [2] g->run()
(int) 42
root [3] .q

(I changed the directory name to match your own)

CMakeLists.txt (568 Bytes)

@bellenot

Working!!! Great!!

Wasn’t working directly, as the last line

ROOT_SET_OUTPUT_DIRECTORIES(root20260116)

is causing an error

PS C:\Users\troska\source\repos\root20260116\root20260116\build\Release> cmake -Wno-dev -G"Visual Studio 17 2022" -A x64 -Thost=x64 -DCMAKE_VERBOSE_MAKEFILE=ON ..\
-- Selecting Windows SDK version 10.0.19041.0 to target Windows 10.0.22631.
CMake Error at CMakeLists.txt:20 (ROOT_SET_OUTPUT_DIRECTORIES):
Unknown CMake command "ROOT_SET_OUTPUT_DIRECTORIES".

-- Configuring incomplete, errors occurred!

I removed it and had to move the pcm and rootmapfiles manually to the Relase-folder.

Anyhow this brings me forward enourmously - thank you

Are you always typing the cmake-stuff in the terminal or have you connected the visual studio build command to the cmake-file somehow? I do not know if there is a straight forward way of doing this

I have never used cmake, I always preferred autogen.sh && ./configure && make

Georg

1 Like

Cool!

Right, sorry, I forgot to remove that line, which is only working with the current head

You’re very welcome! And yes, I’m used to use the command prompt, especially because I’m very often connecting to VMs via ssh. I general I only use Visual Studio for debugging purposes.

Cheers, Bertrand.

Hi @bellenot,

although the original post is answered, I still need some help with your CMakeLists.txt.

I’m not very familiar with CMakeList - so sorry for asking so stupid questions :slight_smile:

My original code is much more than one header file. How can I add the *.cpp files and tell Cmake to create the objects before linking? By know I’m getting Linker errors, as there are unresolved external symbols

Thanks
Georg

add_library(root20260116 SHARED
    File1.cpp
    File2.cpp
    File3.cpp
    FileN.cpp
    DictOutput.cxx
    gttest.h
)

@bellenot:

Sorry, this is not working:

cmake_minimum_required(VERSION 3.16)
set(CMAKE_VERBOSE_MAKEFILE ON)

project(measure LANGUAGES CXX)

find_package(ROOT REQUIRED)
include_directories(${ROOT_INCLUDE_DIRS})
link_directories(${ROOT_LIBRARY_DIR})

set(CMAKE_CXX_FLAGS "${ROOT_CXX_FLAGS}")

add_library(measure SHARED
    ${CMAKE_CURRENT_SOURCE_DIR}/Channel.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/DCELink.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/DCEResource.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/Device.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/HP33120A.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/K2000.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/Limit.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/MeasEvent.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/MeasureChannel.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/Param.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/PosixFile.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/PosixSocket.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/RS232Link.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/Scan1D.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/ScanSave.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/Signal.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/TCPLink.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/Window1DGraph.cpp
)

root_generate_dictionary(DictOutput
    ${CMAKE_CURRENT_SOURCE_DIR}/Channel.h
    ${CMAKE_CURRENT_SOURCE_DIR}/DCELink.h
    ${CMAKE_CURRENT_SOURCE_DIR}/DCEResource.h
    ${CMAKE_CURRENT_SOURCE_DIR}/Device.h
    ${CMAKE_CURRENT_SOURCE_DIR}/ExcLimit.h
    ${CMAKE_CURRENT_SOURCE_DIR}/HP33120A.h
    ${CMAKE_CURRENT_SOURCE_DIR}/K2000.h
    ${CMAKE_CURRENT_SOURCE_DIR}/Limit.h
    ${CMAKE_CURRENT_SOURCE_DIR}/MeasEvent.h
    ${CMAKE_CURRENT_SOURCE_DIR}/MeasureChannel.h
    ${CMAKE_CURRENT_SOURCE_DIR}/Param.h
    ${CMAKE_CURRENT_SOURCE_DIR}/PosixFile.h
    ${CMAKE_CURRENT_SOURCE_DIR}/PosixSocket.h
    ${CMAKE_CURRENT_SOURCE_DIR}/RS232Link.h
    ${CMAKE_CURRENT_SOURCE_DIR}/Scan1D.h
    ${CMAKE_CURRENT_SOURCE_DIR}/ScanSave.h
    ${CMAKE_CURRENT_SOURCE_DIR}/Signal.h
    ${CMAKE_CURRENT_SOURCE_DIR}/TCPLink.h
    ${CMAKE_CURRENT_SOURCE_DIR}/Window1DGraph.h
)
add_library(measure SHARED
    DictOutput.cxx
    ${CMAKE_CURRENT_SOURCE_DIR}/Channel.h
    ${CMAKE_CURRENT_SOURCE_DIR}/DCELink.h
    ${CMAKE_CURRENT_SOURCE_DIR}/DCEResource.h
    ${CMAKE_CURRENT_SOURCE_DIR}/Device.h
    ${CMAKE_CURRENT_SOURCE_DIR}/ExcLimit.h
    ${CMAKE_CURRENT_SOURCE_DIR}/HP33120A.h
    ${CMAKE_CURRENT_SOURCE_DIR}/K2000.h
    ${CMAKE_CURRENT_SOURCE_DIR}/Limit.h
    ${CMAKE_CURRENT_SOURCE_DIR}/MeasEvent.h
    ${CMAKE_CURRENT_SOURCE_DIR}/MeasureChannel.h
    ${CMAKE_CURRENT_SOURCE_DIR}/Param.h
    ${CMAKE_CURRENT_SOURCE_DIR}/PosixFile.h
    ${CMAKE_CURRENT_SOURCE_DIR}/PosixSocket.h
    ${CMAKE_CURRENT_SOURCE_DIR}/RS232Link.h
    ${CMAKE_CURRENT_SOURCE_DIR}/Scan1D.h
    ${CMAKE_CURRENT_SOURCE_DIR}/ScanSave.h
    ${CMAKE_CURRENT_SOURCE_DIR}/Signal.h
    ${CMAKE_CURRENT_SOURCE_DIR}/TCPLink.h
    ${CMAKE_CURRENT_SOURCE_DIR}/Window1DGraph.h
)
set_target_properties(measure PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS TRUE)
target_link_libraries(measure PUBLIC ROOT::Core)```



results in


C:\Users\troska\Desktop\git\measure\build>cmake -Wno-dev -G"Visual Studio 17 2022" -A x64 -Thost=x64 -DCMAKE_VERBOSE_MAKEFILE=ON ..\src\
-- Selecting Windows SDK version 10.0.19041.0 to target Windows 10.0.22631.
CMake Error at CMakeLists.txt:54 (add_library):
  add_library cannot create target "measure" because another target with the
  same name already exists.  The existing target is a shared library created
  in source directory "C:/Users/troska/Desktop/git/measure/src".  See
  documentation for policy CMP0002 for more details.


-- Configuring incomplete, errors occurred!

If you read carefully the error message, you can see: add_library cannot create target "measure" because another target with the same name already exists.
Try:

cmake_minimum_required(VERSION 3.16)
set(CMAKE_VERBOSE_MAKEFILE ON)

project(measure LANGUAGES CXX)

find_package(ROOT REQUIRED)
include_directories(${ROOT_INCLUDE_DIRS})
link_directories(${ROOT_LIBRARY_DIR})

set(CMAKE_CXX_FLAGS "${ROOT_CXX_FLAGS}")

root_generate_dictionary(DictOutput
    ${CMAKE_CURRENT_SOURCE_DIR}/Channel.h
    ${CMAKE_CURRENT_SOURCE_DIR}/DCELink.h
    ${CMAKE_CURRENT_SOURCE_DIR}/DCEResource.h
    ${CMAKE_CURRENT_SOURCE_DIR}/Device.h
    ${CMAKE_CURRENT_SOURCE_DIR}/ExcLimit.h
    ${CMAKE_CURRENT_SOURCE_DIR}/HP33120A.h
    ${CMAKE_CURRENT_SOURCE_DIR}/K2000.h
    ${CMAKE_CURRENT_SOURCE_DIR}/Limit.h
    ${CMAKE_CURRENT_SOURCE_DIR}/MeasEvent.h
    ${CMAKE_CURRENT_SOURCE_DIR}/MeasureChannel.h
    ${CMAKE_CURRENT_SOURCE_DIR}/Param.h
    ${CMAKE_CURRENT_SOURCE_DIR}/PosixFile.h
    ${CMAKE_CURRENT_SOURCE_DIR}/PosixSocket.h
    ${CMAKE_CURRENT_SOURCE_DIR}/RS232Link.h
    ${CMAKE_CURRENT_SOURCE_DIR}/Scan1D.h
    ${CMAKE_CURRENT_SOURCE_DIR}/ScanSave.h
    ${CMAKE_CURRENT_SOURCE_DIR}/Signal.h
    ${CMAKE_CURRENT_SOURCE_DIR}/TCPLink.h
    ${CMAKE_CURRENT_SOURCE_DIR}/Window1DGraph.h
)
add_library(measure SHARED
    DictOutput.cxx
    ${CMAKE_CURRENT_SOURCE_DIR}/Channel.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/DCELink.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/DCEResource.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/Device.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/HP33120A.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/K2000.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/Limit.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/MeasEvent.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/MeasureChannel.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/Param.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/PosixFile.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/PosixSocket.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/RS232Link.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/Scan1D.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/ScanSave.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/Signal.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/TCPLink.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/Window1DGraph.cpp
)
set_target_properties(measure PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS TRUE)
target_link_libraries(measure PUBLIC ROOT::Core)

this looks better
now I need to fix 100 compiler errors thanks for your help

Georg