Mi entorno de desarrollo con C++ en Windows

Por Arrecio


Cuando hablamos de lenguajes de programación, y C++ lo es, hablamos de un estándar para crear programas a partir de ficheros de texto plano. Un lenguaje de programación viene a ser un conjunto de especificaciones que cómo mínimo consisten en una sintaxis y una librería estándar. Con la sintaxis definimos la forma en la que escribimos y organizamos el código fuente, y la librería estándar son una serie de funciones básicas con las que interactuar con el sistema subyacente y con las que se proveen funcionalidades básicas para construir nuestros programas. Además el estándar puede establecer más reglas como por ejemplo la forma en la que los datos se representan en memoria, cómo se estructura en su forma binaria, etc, etc.

Entre los propios lenguajes de programación se distinguen entre los de bajo nivel y alto nivel pudiendo decirse que las de alto nivel son más cercanos al lenguaje natural y los de bajo nivel más cercanos al lenguaje máquina. C++ es un lenguaje de alto nivel.

Si hablamos de C++, el estándar del lenguaje es definido por la ISO/IEC teniendo número ISO/IEC 14882 siendo la última versión a fecha de escribir este artículo la de 2024. La denominación final del estándar es ISO/IEC 14882:2024 y aunque es mayormente conocida como la C++23.

Por poner otro ejemplo de lenguaje, el lenguaje C se rige por el estándar ISO/IEC 9899:2024. C y C++ son lenguajes vinculados, y la librería estándar de Cestá disponible, salvo algunas excepciones, en C++

C++ es un lenguaje compilado y para obtener el ejecutable final de un determinado proyecto es necesario una serie de pasos en la que intervienen distintas herramientas de linea de comandos (preprocesadores, compiladores, enlazadores, etc) que funcionan de manera sustancialmente distinta a los lenguajes tipo script (Python o JavaScript, por ejemplo) que son los que más de moda están. A este conjunto de herramientas se las llama toolchains y este es un concepto que debemos tener muy en cuenta durante toda esta entrada del blog ya que cuando hablamos de compiladores de C++ estamos realmente hablando de toolchains de C++.

Los depuradores o debuggers normalmente no son considerados parte del toolchain ya que no forman parte de la cadena de generación del resultado.

Puestos a introducir conceptos debemos hablar también del build tool que son herramientas que mediante archivos de configuración realizan todas las tareas necesarias para generar los ejecutables (o en su caso también librerías) utilizando de manera organizada las herramientas del toolchain.

Hablando ya del lenguaje C++, lo mejor antes de seguir es echar un vistazo rápido a C++ Referencia para hacernos una idea de lo que lo tenemos entre manos. La documentación de Microsoft C++ es otro lugar que poder visitar.

Sentando las bases

Hacer un programa en C++ básicamente significa crear archivos de texto debidamente estructurados, que respetan la sintaxis de este lenguaje y que mediante un proceso de compilación adecuado (mediante las toolchain) producen un archivo que puede ser o bien ejecutado en caso de los ejecutables, o bien incorporado a otros programas en caso de las librerías. Los ejecutables corren en la capa de aplicación del Sistema Operativo o SO.

Simplificando al máximo la estructura de un SO este estaría formado básicamente por 3 capas, la más interna es el núcleo que es la parte que está en contacto directo con el hardware, la que realiza las partes más específicas y más difíciles de entender por el usuario. Una capa intermedia sería la API del sistema que proporciona abstracción a la hora de comunicarnos con núcleo del sistema operativo quien a su vez proporciona abstracción para comunicarnos con el hardware. Por ejemplo una operación de escritura en disco tendrá siempre la misma llamada a la API del sistema, independientemente de la marca y modelo del disco. Lo mismo por ejemplo una llamada a que dibuje un cuadrado en la pantalla, que será independiente del hardware gráfico.

La capa superior es la de aplicación, en la que se sitúan los programas que hacemos los usuarios pero también otros muchos que provee el propio SO como el explorador de archivos, programas para configurar el propio sistema operativo, u otras utilidades. Entre esos programas está el intérprete de comandos, o CLI de Command Line Interface y la interfaz gráfica de usuario, o GUI de Graphics User Interface, que son las vías con las que ejecutamos el resto de programas.

Esto puede verse mejor en el siguiente dibujo que evidentemente se podría complicar mucho más:

Estructura básica de sistema operativo

La forma binaria de los programas de usuario dependerá del sistema operativo, y en el caso de Vindows esa forma tiene nombre de formato Portable Ejecutable o PE format. El resultado final de un programa compilado debe ser esa forma binaria dependiente del SO por lo que el proceso de compilado debe ser específico para ese sistema operativo. Para curiosos hay programas como PETools que permiten husmear en la estructura de los ejecutables de Windows.

Esta forma de programar es sustancialmente distinta a la de los lenguajes interpretados, en los que el código se pasa un programa llamado intérprete que es el que realmente corre en esa capa de aplicación del Sistema Operativo. Por ejemplo para el caso de Python, aunque existen varios intérpretes, el más utilizado es CPython, que es un intérprete programado en C y que por tanto en algún momento sufrió un proceso de compilación. Los scripts de Python se cargan en el intérprete y es el intérprete quien interactúa con la API en caso de ser necesario.

Dentro de la capa superior incluiríamos también a las librerías de usuario, que son otra forma del resultado de compilación cuyo objetivo es seguir el principio de reutilización y evitar reinventar la rueda. Las librerías son representaciones binarias de software que no está pensado para ejecutarse sino más bien para proporcionar funcionalidades a otros programas.

No es el objeto de este artículo el entrar en como se utilizan las librerías pero uno de los pasos finales del proceso de compilación es el de enlazado denominándose enlazador o linker a esa parte del toolchain que se encarga de fusionar la parte binaria del código que hemos escrito con la parte binaria del código externo que se aloja en las librerías.

Existen dos procesos de enlazado, el estático por el cual todo el código de la librería se incrusta directamente en el ejecutable final y simplemente se mapean las direcciones de memoria para acceder a las funciones de las librerías en el preciso lugar en el que ha quedado encajadas, y luego está el enlace dinámico por el cual lo que se incrusta en el ejecutable es una serie de reglas en forma de interfaz mucho más liviana, para poder acceder a la librería a través del sistema operativo. En el caso de los SO Windows estas librerías tiene forma de DLL, en Linux por ejemplo son SO, pero de Shared Object y no de Sistema Operativo.

En el caso de las librerías dinámicas la parte que se incrusta en el ejecutable no es la parte binaria de librería que proporciona su funcionalidad sino la parte binaria que funciona como importador de la misma, dicho importador lo que proporciona es la manera de acceder a las funcionalidades que están en la biblioteca dinámica en un fichero a parte.

Hablando de Windows las librerías estáticas se componen de un único archivo .lib que implementa dichas funcionalidades, mientras que las dinámicas existe también .lib que actúa de importador y un archivo .dll que donde residen las funcionalidades provistas. En ambos casos deben estar acompañadas de los archivos de cabecera correspondientes. Un aspecto importante a señalar es que en el caso de los archivos .lib se requiere para usarlos que hayan sido compiladas con la misma toolchain, o una 100% compatible.

Una de las bondades del uso de librerías dinámicas es que las mismas pueden compartirse entre procesos, haciendo que los ejecutables sean menos pesados, aunque también existen prácticas de distribuir las DLL junto con los ejecutables y al final puedes encontrarte el sistema de archivos con varias copias de las mismas librerías, lo que puede terminar en conflictos si se encuentra varías versiones de estas y un programa intenta cargar alguna versión incompatible. Ver DLL Search Order y el comando where para intentar solventar problemas.

La librería estándar de C/C++

Sí, estamos hablando de C++, pero este lenguaje se ideó como un super-lenguaje de C. En el caso de Windows la librería estándar de C forman parte de Microsoft Visual C++ Runtime Environment y adelanto que existen dos implementaciones distintas: MSVCRT y UCRT en forma de distintos archivos DLL. En cuanto a la librería estándar de C++, Microsoft provee la Standard Template Library.

Siguiendo el modelo de capas visto en la sección anterior, cuando un programa de usuario quiere usar los recursos que le proporciona el SO debe hacer uso de su API. En el caso de Windows, la Windows API está formada por una serie de librerías de enlace dinámico no existiendo alternativa de enlazado estático.

Aunque es posible vincular las DLL de la API de Windows a programas C++ y usarlos directamente, hacer esto convertiría al programa en poco o nada portable ya que dependería exclusivamente de unas determinadas librerías únicamente disponibles en un SO específico.

Es por ello que existen las librerías estándar, que sirven como puente entre el código de nuestro programa de usuario y la API del sistema operativo. Esta librería estándar se utilizará eminentemente de manera dinámica, incluso aunque exista la posibilidad de enlazarla de manera estática, y ya no sólo por el ahorro de espacio. Enlazarla dinámicamente permite que el distribuidor del sistema operativo pueda actualizarla para resolver bugs o mejorar el rendimiento o la seguridad, mientras que si se enlazara estáticamente el programa correría siempre la versión de la librería estándar usada en el momento de la compilación.

Las buenas prácticas de programación harán que siempre que queramos hacer uso del SO y existe un método para hacerlo a través de la librería estándar se usará ese método. En la práctica un programa ejecutable de Windows escrito en C o en C++ estará siempre enlazado dinámicamente a la librería estándar de C (MSVCRT o al UCRT dependiendo de la elegida), y quizás a más librerías dinámicas. Las llamadas entre la librería estándar y la API de Windows es totalmente transparente y el ejecutable ni tan siquiera figura vinculado a ellas.

Programar C++ con Windows (by Microsoft)

Para programar en C++ en Windows no cabe duda que la opción más amigable es la de usar el IDE Visual Studio y especialmente para el usuario no profesional su versión Community que utiliza MSVC como toolchain y MSBuild como build tool.

Se instala a través de Visual Studio Installer debiendo seleccionar los componentes que necesitaremos para lo que usaremos simplemente el sentido común. Todo es bastante sencillo aunque una vez obtenido el IDE hay que tener un conocimiento profundo de como funciona el lenguaje para hacer programas que impliquen el trabajo con librerías ya que debemos configurar diversos aspectos acerca de la importación de cabeceras y enlaces con los binarios correspondientes, algo que será trivial si utilizamos el gestor de paquetes vcpkg del que hablaremos en más adelante una vez que puede usarse con otros compiladores.

En cuanto a la librería estándar de C/C++ utilizada por los compiladores de Microsoft ha sido históricamente MSVCRT pero desde Windows 10 está disponible también UCRT, que es la recomendada. La versión que utilizaremos para compilar básicamente depende de la suite de Visual Studio utilizada.

No obstante no quiero usar Visual Studio, y no es por cuestiones éticas, es simplemente por capricho. Como he dicho en alguna ocasión no me dedico profesionalmente al mundo de las TIC, para mi es un hobby y hacerlo de esta manera me resulta más divertido y apasionante.

MSYS2 la opción elegida

Tampoco me he complicado mucho la vida. La historia de los entornos de desarrollo de aplicaciones con C++ estuvo marcada inicialmente por Microsoft C++ que ahora es Microsoft Visual C++ dentro de la suite Visual Studio y Borland C++ cuya vida se prolonga también hasta nuestros días como Embarcadero C++Builder.

En paralelo a ellos, en los sistemas *NIX existen también compiladores de todos los colores entre los que destaca GCC.

En 1995 apareció Cygwin que prometía ser, y lo es, una capa de compatibilidad Linux dentro de Windows. No permitía correr programas nativos de Linux pero era un soporte para que se pudieran compilar programas escritos para Linux (que usaban API de Linux) desde Windows, y en general es un entorno que te hace sentir estar en Linux (su eslogan es "Get that Linux feeling - on Windows"). El soporte a todas las llamadas nativas al sistema Linux lo proporcionaba una DLL llamada cygwin1.dll que emula todo el sistema POSIX subyacente a los programas Linux. Además Cygwin viene acompañado de la obligada colección de herramientas GNU corriendo en Windows.

Entre esas herramientas esta lógicamente GCC así como GDB o make. Aquí es donde entra MinGW que es el componente de Cygwin que provee de todas esas herramientas para compilar y depurar programas cuyo SO objetivo era Linux. Se dice que MinGW es una suite de herramientas desarrollo de aplicaciones C/C++ que usa la toolchain GCC y la build tool make. Es decir que MinGW es más que una toolchain.

Que los formatos (COFF (de los *NIX) y PE (de Windows) sean similares facilita bastante las cosas.

Aunque Cygwin sigue siendo una opción viable hoy en día como entorno GNU y para compilar con GCC programas que corren en Windows (su última versión estable ha sido anunciada el 2 de marzo de 2026) la realidad es que se ha visto superada por MSYS2. En su web explican las diferencias con Cigwin.

Si Cygwin estaba construido entorno a MinGW, MSYS2 lo está entorno a Mingw-w64 es un fork de MinGW (producido allá por 2010 cuando Mingw se resistía a portarse a 64 bits).

Al igual que MinGW, Mingw-w64 es una suite de herramientas de compilación que igualmente incluye GCC, GDB y make, así como otras más. La gran diferencia entre Mingw-w64 y MinGW es que mientras que este último no produce aplicaciones puramente nativas de Windows, Mingw-w64 sí.

Mejor me explico. Ya he hablado que los programas compilados en C/C++ dependen de su librería estándar, a través de la cual acceden a la API del SO. En el caso de Mingw-w64 los programas que genera utilizan la librería estándar provista por Microsoft, lo que a día de hoy significa que pueden usar MSVCRT o UCRT, que ya hemos nombrado.

Sin embargo MinGW accede a la API de Windows también mediante cygwin1.dll donde se implementa la librería estándar de C newlib. Eso implica que esa librería debe acompañar siempre a nuestro ejecutable.

Mingw-w64 no sólo corre en Windows, puede correr en otros muchos sistemas desde lo que crear aplicaciones nativas windows mediante un proceso que se conoce como cross-compiling. En su web podemos encontrar una lista de todas las implementaciones binarias disponibles.

Además no confundir Cigwin o MSYS2 con WSL. Los primeros son entornos de desarrollo que corren nativamente sobre Windows, y lo segundo es una plataforma de virtualización.

Pero MSYS2 no se limita únicamente a ser una un entorno GNU para linux así como plataforma de desarrollo de aplicaciones C/C++ con GCC. MSYS2 provee una extensa colección de paquetes que se gestionan con pacman, que le resultará familiar a los usuarios de ArchLinux ya que es el usado en este SO. Con él podremos, además de descargar los toolchains, obtener otras muchas utilidades como depuradores y lo más importante, las librerías precompiladas para las distintas toolchains que queramos usar.

He dicho distintas toolchains ya que además de GCC podemos usar Clang dentro de MSYS2, sin contar que además es Cygwin compatible. Para gestionar bien el uso correcto de las herramientas MSYS2 se estructura en entornos MSYS2 los cuales residen cada uno en una rama de directorios de la raíz de MSYS2 desde los que cuelgan sus herramientas de compilación, depurado, etc, específicas así como donde se alojan las librerías compatibles con ellas. Vemos en el último enlace que algunos de estos entornos se consideran legacy por lo que no son los que debiéramos usar para nuevos proyectos a no ser que existan motivos para ello.

Por terminar este punto indicar que debemos distinguir entre herramientas de entorno y del propio MSYS2 las cuales son provistas por paquetes distintos. Por ejemplo existe el paquete GCC para los entornos y para MSYS2 (base pakages). El de los entornos está pensado para ser utilizado desde fuera para generar los programas nativos, el de MSYS2 está usado para usarlo desde dentro, desde las ventanas de comandos de MSYS2, y por tanto para generar programas "nativos" de MSYS2.

Los paquetes para ser usados directamente desde MSYS2 tienen el nombre tal cual, mientras que los de los entornos comienzan usualmente por mingw-w64-<entorno>-<arch>.

Instalando UCRT64 toolchain

Lo primero que tenemos que saber trabajando con MSYS2 es que no debemos caer en la tentación de utilizar Mingw64 como toolchain pensando que es la "oficial" de Mingw-w64 cuando realmente es una versión considerada como antigua. Esto es tan sólo el nombre del entorno. En MSYS2 todo es Mingw-w64.

La propia página de los entornos MSYS2 explica que los mismos consisten es configuraciones distintas del entorno Mingw-w64, los cuales se instalan en directorios distintos dentro del raíz del propio MSYS2. Cuando instalamos paquetes en MSYS2 si estos tienen en su nombre la denominación de un entorno es porque se van a incluir en su rama de directorio correspondiente y sólo estarán disponibles cuando estemos usado dicho entorno.

El entorno que voy a utilizar es UCRT64 que utiliza GCC como toolchain y lógicamente libstdc++ de librería estándar de C++, no obstante en cuanto a la librería de C utiliza UCRT. Ya que hemos escogido UCRT no estaría de más echar un vistazo a esta introducción.

Instalado MSYS2 se nos ofrece abrir la ventana de comandos que será por defecto la del entorno UCRT64, si bien puede hacerse lo siguiente con cualquiera de ellas:

pacman -Syu
pacman -S mingw-w64-ucrt-x86_64-toolchain

mingw-w64-ucrt-x86_64-toolchain es en realidad un meta-paquete que instala todo lo necesario para compilar programas en C++. Notar que aunque todas estas herramientas hayan sido instaladas no están disponibles desde esa línea de comandos ya su finalidad es usarlas desde el entorno Windows.

No obstante es probable que queramos descargar alguna build tool y en caso descargaré CMake pero no de su web sino de los paquetes disponibles en MSYS2 y ya de paso el depurador GDB.

pacman -S mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-gdb

La build tool que vamos a usar es realmente ninja pero es que no la vamos a usar directamente sino a través de CMake, que ya hemos instalado previamente y que no se tiene como una build tool sino como una meta-build Tool por entenderse que está una capa por encima de la anterior. De hecho tiene la capacidad de generar proyectos de Visual Studio.

Todos estos ejecutables se instalan en C:\msys64\ucrt64\bin, directorio que debemos añadir al PATH de Windows para que estén disponibles dentro de su entorno. Para facilitar las cosas pongo el comando que podemos ejecutar en un terminal de Windows en el que estemos ejecutado CMD para incluirlo. Ojo si lo haces con PowerShell debes usar $env:PATH en lugar de %PATH o romperás tu anterior lista de PATH:

setx PATH "%PATH%;C:\msys64\mingw64\bin"

En C:\msys64\usr\bin se encuentran las herramientas GNU por lo que también es una buena opción incorporarlas al PATH ya que la mayoría de ellas funcionan sin problemas en el entorno Windows aunque lo normal será usarlas con un shell de MSYS2.

Si tienes instalado WSL en tu sistema debes tener en cuenta que esto instala su propia versión de Mingw-w64 por lo que es posible que quieras eliminar del PATH "C:\ProgramData\mingw64\mingw64\bin" para evitar conflictos.

Reiniciamos el terminal y ejecutamos:

clang --version
cmake --version
gcc --version
g++ --version
gdb --version
make --version

Ante lo que no deberíamos obtener ningún error. Para ver que estemos usando las utilidades que queremos usar podemos ejecutar en un terminal PowerShell lo siguiente:

Get-Command g++

Debiendo decirnos que estamos usando g++.exe de C:\msys64\mingw64\bin.

¿IDE o no IDE?

No querer usar Visual Studio no implica que no pudiera usar otros IDE como CLion, Eclipse, Xcode (MAC) o QT Creator (este más específico).

Usar un IDE no es estrictamente necesario a día de hoy ya que existen plugins para editores como VSCode o Vim/NeoVim que facilitan bastante el trabajo.

Yo en principio voy a usar VScode para lo que recomiendo seguir estos pasos aunque muchos de ellos ya los hemos hecho antes. Decir que la extensión C++ instala la extensión CMake Tools.

Puede que notes que al ejecutar "g++" o "clang" desde un terminal de VSCode se informa de que los ejecutables no han sido encontrados. Si has cambiado el PATH mientras tenías alguna ventana de VSCode abierta no se actualizará automáticamente en sus terminales. Deberás o matar todos los procesos del editor o abrir un terminal de Windows y ejecutar "code" desde ahí. Otra opción que puede servir para en otros supuestos es seguir lo que dicen aquí.

Gestor de librerías (el oficial)

MSYS2 tiene una completa lista de librerías disponibles y su instalación con pacman debería ser la primera opción. Para buscar librerías usamos la opción -Ss podemos usar el siguiente comando (para buscar boost, que seguramente terminemos usando alguna vez):

pacman -Ss boost

En mi caso obtengo el siguiente resultado:

clangarm64/mingw-w64-clang-aarch64-boost 1.90.0-3
    Free peer-reviewed portable C++ source libraries (mingw-w64)
clangarm64/mingw-w64-clang-aarch64-boost-libs 1.90.0-3
    Free peer-reviewed portable C++ source libraries (runtime libraries) (mingw-w64)
mingw32/mingw-w64-i686-boost 1.90.0-3
    Free peer-reviewed portable C++ source libraries (mingw-w64)
mingw32/mingw-w64-i686-boost-libs 1.90.0-3
    Free peer-reviewed portable C++ source libraries (runtime libraries) (mingw-w64)
mingw64/mingw-w64-x86_64-boost 1.90.0-3
    Free peer-reviewed portable C++ source libraries (mingw-w64)
mingw64/mingw-w64-x86_64-boost-libs 1.90.0-3
    Free peer-reviewed portable C++ source libraries (runtime libraries) (mingw-w64)
ucrt64/mingw-w64-ucrt-x86_64-boost 1.90.0-3
    Free peer-reviewed portable C++ source libraries (mingw-w64)
ucrt64/mingw-w64-ucrt-x86_64-boost-libs 1.90.0-3
    Free peer-reviewed portable C++ source libraries (runtime libraries) (mingw-w64)
clang64/mingw-w64-clang-x86_64-boost 1.90.0-3
    Free peer-reviewed portable C++ source libraries (mingw-w64)
clang64/mingw-w64-clang-x86_64-boost-libs 1.90.0-3
    Free peer-reviewed portable C++ source libraries (runtime libraries) (mingw-w64)

Evidentemente debemos asegurarnos de que instalamos la de nuestro entorno ya que en caso contrario no serán encontradas:

pacman -Sy mingw-w64-ucrt-x86_64-boost

Más adelante hablaré de como usarlas y enlazarlas mediante CMake lo que no requerirá pasos adicionales. Adelanto que el comando find_library de los archivos de configuración de CMake siempre intentará localizar las librerías en los repositorios de MSYS2 (del entorno de trabajo) una vez que hemos instalado CMake como paquete suyo. Desconozco como se procedería en caso de usar otra versión.

Gestor de librerías (el alternativo)

Ya he nombrado en este capítulo vcpkg. Vcpkg es el gestor de paquetes de VC++ pero puede ser utilizados con otros compiladores a través de CMake.

El siguiente dibujo representa un poco como funciona vckpg:

Cómo funciona vcpkg

Básicamente es un sistema formado por una estructura de directorios y un ejecutable que es vcpkg.exe. Cuando se desea instalar un paquete, que básicamente consiste en una librería C++, se bajan sus fuentes y se utiliza el toolchain de destino para compilarlas. Para su uso con las diferentes build tools será necesario realizar algún paso pero en lo que respecta a su uso con Visual Studio C++ este será único. En el caso VSCode + CMake será necesario realizar configuraciones específicas para cada proyecto.

Para usar vcpkg seleccionaremos un directorio de nuestro sistema para usarlo de repositorio, por ejemplo D:\vcpkg\, y teniendo claro esto hacemos desde un terminal de Windows:

git clone https://github.com/microsoft/vcpkg D:\vcpkg

Tras esto vamos hasta el directorio elegido y ejecutamos .\bootstrap-vcpkg.bat para obtener el binario. Lo vamos a meter además en el PATH mediante los siguientes comandos:

setx VCPKG_ROOT "D:\vcpkg"
setx PATH "%PATH%;%VCPKG_ROOT%"

Para probar vcpkg voy a instalar una librería que tengo pensado utilizar más adelante en uno de mis proyectos (GLFW). Es necesario especificar el toolchain de destino que en caso de vcpkg recibe, por alguna razón que desconozco, el nombre de triplet:

./vcpkg install glfw3 --triplet x64-mingw-dynamic --host-triplet x64-mingw-dynamic

o más fácil:

./vcpkg install glfw3:x64-mingw-dynamic

Las librerías quedan instaladas en D:\vcpkg\installed en el subdirectorio correspondiente al compilador indicado y decir que en este caso hemos bajado una versión para utilizarse con Mingw-w64 y mediante enlazado dinámico en contraposición al enlace estático que sería con static.

Si no especificamos ningún valor al triplet se utilizará un valor por defecto que en Windows será x64-windows (). Si queremos que por defecto se instalen las librerías para Mingw-w64 y enlazado dinámico debemos establecer ciertas variables del entorno (yo no lo voy a hacer):

setx VCPKG_DEFAULT_TRIPLET "x64-mingw-dynamic"
setx VCPKG_DEFAULT_HOST_TRIPLET "x64-mingw-dynamic"

Uso de vcpkg en VSCode con CMake

Para usar vcpkg con CMake su uso no es tan trivial como en el caso de MSBuild. La configuración debe hacerse por proyectos, aplicada a su archivo de configuración. Para ello vamos a crear un proyecto de prueba que comienza con la creación de una carpeta vacía en cualquier lugar de nuestro sistema de archivos. Básicamente vamos a replicar lo dicho aquí con alguna salvedad.

Desde una terminal y desde ese directorio hacemos:

vcpkg new --application
code .

En el explorador de archivos de VSCode vemos un archivo vcpkg.json que de momento está vacío pero si volvemos a la consola y hacemos por ejemplo:

vcpkg add port fmt

Vemos que su contenido ha cambiado:

{
  "dependencies": [
    "fmt"
  ]
}

Pero aquí no acaba la cosa. Creamos un archivo CMakeLists.txt junto al archivo json, que es el archivo director de CMake. Le insertamos el siguiente contenido:

cmake_minimum_required(VERSION 3.10)

project(HelloWorld)

find_package(fmt CONFIG REQUIRED)

add_executable(HelloWorld helloworld.cpp)

target_link_libraries(HelloWorld PRIVATE fmt::fmt)

Podemos consultar todos los comandos insertados en este manual. Pongo los enlaces a los aquí utilizados: cmake_minimum_required, project, find_package, add_executable y target_link_libraries.

El más relevante de los comandos es find_package que está vinculado con las dependencias del proyecto de lo que pronto hablaré.

Si hubiera instalado el paquete mingw-w64-ucrt-x86_64-fmt con pacman dentro de MSYS2 no sería necesario realizar estos pasos ya que find_package lo encontraría sin problemas, pero estamos ilustrando el funcionamiento con vcpkg.

Primero vamos a añadir código en el proyecto ahora el típico helloworld.cpp en el que efectivamente hagamos uso de la librería:

#include <fmt/core.h>

int main()
{
    fmt::print("Hello World!\n");
    return 0;
}

Para entender como continuar lo mejor es probar a compilar el proyecto usando CMake con lo que tenemos ahora mismo:

cmake .\CMakelists.txt

Ante lo que deberíamos obtener un error al no poder find_package encontrar la dependencia fmt. Lo que queremos es resolver dependencias usando Cmake a través de vcpkg. CMake puede usar herramientas auxiliares para resolver dependencias mediante archivos denominados CMAKE_TOOLCHAIN_FILE, y vcpk tiene su propio script para ello en la ruta scripts/buildsystems/ del directorio de trabajo de vcpkg.

Para utilizarlo hay varias alternativas pero lo habitual será a hacerlo a través de los CMake presets para lo que vamos a utilizar ficheros JSON auxiliares conformes a las especificaciones indicadas en la página del manual recién referenciada.

Con los presets podremos crear diferentes configuraciones de configuración usando CMake, pudiendo coexistir presets para distintos objetivos. En nuestro caso vamos a usar un preset en un único archivo CMakePresets.json que va a quedar así:

{
  "version": 2,
  "configurePresets": [
    {
      "name": "test",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build",
      "cacheVariables": {
        "CMAKE_TOOLCHAIN_FILE": "d:/vcpkg/scripts/buildsystems/vcpkg.cmake",
        "VCPKG_TARGET_TRIPLET": "x64-mingw-static",
        "VCPKG_HOST_TRIPLET": "x64-mingw-static"
      }
    }
  ]
}

Con este archivo creamos un preset llamado test para el que hemos configurado diversas variables que se pasarán a CMake de la misma manera que se podrían haber establecido por línea de comandos con la opción -D o mediante el comando set en CMakelist.txt pero esto último es más férreo que establecerlo en los presets y lo de la línea de comandos es más tedioso.

Con esto me he separado un poco del tutorial que he estado siguiendo. En ese se utiliza CMakePresetsUser.json y lo hago para ilustrar que en soluciones pequeñas no es necesario más que lo que estamos haciendo.

Para más información respecto a los presets la documentación de Visual Studio tiene un artículo bastante resumido pero suficiente explicativo.

Con esta configuración podemos iniciar la compilación con el siguiente comando:

cmake --presets=test

No deberíamos obtener ningún error. Con esto CMake deja el resultado de su trabajo preparando las dependencias en la carpeta build. Allí deben estar las fuentes y binarios de la librería fmt descargada a través de vcpkg. Un fichero interesante es build.ninja que es utilizado por el Ninja build tool con el que trabaja CMake.

Para finalmente compilar el proyecto y comprobar que todo ha ido bien:

cmake --build build
.\build\HelloWorld.exe

Debug con Visual Studio Code

La documentación de VSCode contiene una entrada para la configuración del depurado la cual se realiza mediante el archivo launch.jon alojado en el directorio .vscode de nuestro proyecto.

En situaciones sencillas como la de el ejemplo de hola mundo se puede usar la tecla F5 que inicia la depuración para el archivo abierto, si es un fichero .cpp se nos dará la opción C++ (GDB/LLDB) seguidas de subopciones en la que podemos elegir C/C++: g++.exe build and debug active file, pudiendo salir distintas opciones asegurándonos de que utilizamos aquella que g++ sea el alojado en nuestro entorno MSYS2.

Esto nos creará un archivo task.json en el directorio .vscode. Esto es para la tarea de compilación, que no de depuración:

{
    "tasks": [
        {
            "type": "cppbuild",
            "label": "C/C++: g++.exe build active file",
            "command": "C:\\msys64\\ucrt64\\bin\\g++.exe",
            "args": [
                "-fdiagnostics-color=always",
                "-g",
                "${file}",
                "-o",
                "${fileDirname}\\${fileBasenameNoExtension}.exe"
            ],
            "options": {
                "cwd": "C:\\msys64\\ucrt64\\bin"
            },
            "problemMatcher": [
                "$gcc"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "detail": "Task generated by Debugger."
        }
    ],
    "version": "2.0.0"
}

En un proyecto sencillo como nuestro Hello World no será necesario realizar mucho más y teniendo como fichero activo "HelloWorld.cpp" y pulsando F5 se compila HelloWorld.exe en el mismo directorio y comenzará la depuración. Esta operación de depuración es posible por que que pasa la opción -g a g++.

Para ejecutar la tarea por separado podemos usar Ctrl+Shift+B lo que generará el ejecutable con información de depuración sin hacer nada más, pero ni rastro de un archivo launch.json, lo cual entiendo que es debido a que se usa la configuración por defecto asociada a la tarea de compilación.

No obstante en la mayoría de los casos no querremos depurar el archivo activo porque seguramente forme parte de un proyecto mucho más complejo. Entonces sí que queremos ese archivo launch.json.

Creado el launch.json el comportamiento de F5 es utilizarlo, por lo que cuando existe un launch.json aparentemente se ignora el task.json al usar tal tecla.

En lugar de construirlo por nuestra cuenta podemos generar un archivo launch.json desde una plantilla usando el icono del engranaje de las utilidades que por defecto están en el extremo derecho de la barra de tabs con los archivos abiertos. La que dice Add Debug Configuration y seleccionando (gdb) Launch. Esto nos genera un launch.json asi:

{
    "configurations": [
        {
            "name": "(gdb) Launch",
            "type": "cppdbg",
            "request": "launch",
            "program": "enter program name, for example ${workspaceFolder}/a.exe",
            "args": [],
            "stopAtEntry": false,
            "cwd": "${fileDirname}",
            "environment": [],
            "externalConsole": false,
            "MIMode": "gdb",
            "miDebuggerPath": "/path/to/gdb",
            "setupCommands": [
                {
                    "description": "Enable pretty-printing for gdb",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                },
                {
                    "description": "Set Disassembly Flavor to Intel",
                    "text": "-gdb-set disassembly-flavor intel",
                    "ignoreFailures": true
                }
            ]
        }
    ],
    "version": "2.0.0"
}

Pero para poder depurar así necesitamos que el ejecutable al que apunte "program" esté compilado en modo debug, ese que la tarea de compilación realizaba al pasar el parámetro -g a g++. Lo que vamos a hacer primero es configurar CMake para que produzca una versión de nuestro ejecutable que incorpore código que permita la depuración ya que los ejecutables que no lo contienen no pueden ser utilizados para ello. Modificamos nuestro CMakePresets.json de la siguiente manera:

{
  "version": 2,
  "configurePresets": [
    {
      "name": "default",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/release",
      "cacheVariables": {
        "CMAKE_TOOLCHAIN_FILE": "d:/vcpkg/scripts/buildsystems/vcpkg.cmake",
        "VCPKG_TARGET_TRIPLET": "x64-mingw-static",
        "VCPKG_HOST_TRIPLET": "x64-mingw-static"
      }
    },
    {
      "name": "debug",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/debug",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug",
        "CMAKE_TOOLCHAIN_FILE": "d:/vcpkg/scripts/buildsystems/vcpkg.cmake",
        "VCPKG_TARGET_TRIPLET": "x64-mingw-static",
        "VCPKG_HOST_TRIPLET": "x64-mingw-static"
      }
    }
  ]
}

Además borramos por completo nuestro directorio ./build ya que hemos realizado cambios en relación a donde se alojarán los archivos de compilación. Hacemos ahora:

cmake --preset=debug
cmake --build build/debug
.\build\debug\HelloWorld.exe

Si hacemos lo mismo con el preset release observaremos que el tamaño del ejecutable es algo mayor en la versión debug.

Modificamos nuestro launch.json para que "program" y "miDebuggerPath" indiquen "${workspaceFolder}/build/debug/Hello World.exe" y "C:/msys64/ucrt64/bin/gdb.exe" respectivamente.

Si tras esto pulsamos F5 comenzará la depuración, deteniendo la misma en los brakepoints que configuremos pero cada vez que hagamos cambios en el proyecto deberemos compilar por nuestra cuenta el ejecutable final antes de realizar de nuevo la tarea de depuración existiendo la opción de automatizar esto configurando el valor "preLaunchTask" de nuestro launch.json.

Pero en este caso nuestra tarea de compilar el archivo activo no nos sirve. Lo que hemos visto en este apartado como tarea de compilación en realidad es una tarea de VSCode para conectarse a una herramienta externa, que en este caso era C++. La tarea vista era de tipo "cppbuild" que entiendo que se ejecuta a través de la extensión de C++ de VSCode.

Existe un tipo de tarea más genérica que es de tipo shell que nos permite simplemente ejecutar algo desde la línea de comandos. Como la tarea de compilación que hemos usado hasta ahora consiste en usar CMake desde línea de comandados podemos fácilmente automatizar esto antes de lanzar la tarea de depurado.

Nuestro task.json va a quedar ahora como sigue:

{
    "tasks": [
        {
            "type": "shell",
            "label": "CMake debug build",
            "command": "C:\\msys64\\ucrt64\\bin\\cmake.exe --preset=debug && C:\\msys64\\ucrt64\\bin\\cmake.exe --build build/debug",
            "options": {
                "cwd": "${workspaceFolder}"
            },
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "detail": "CMake debug build task"
        }
    ],
    "version": "2.0.0"
}

Dejando finalmente nuestro launch.json tal que sigue:

{
    "configurations": [
        {
            "name": "(gdb) Launch",
            "type": "cppdbg",
            "request": "launch",
            "program": "${workspaceFolder}/build/debug/Overlua.exe",
            "args": [],
            "stopAtEntry": false,
            "cwd": "${fileDirname}",
            "environment": [],
            "externalConsole": false,
            "MIMode": "gdb",
            "miDebuggerPath": "C:/msys64/ucrt64/bin/gdb.exe",
            "preLaunchTask": "CMake debug build",
            "setupCommands": [
                {
                    "description": "Enable pretty-printing for gdb",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                },
                {
                    "description": "Set Disassembly Flavor to Intel",
                    "text": "-gdb-set disassembly-flavor intel",
                    "ignoreFailures": true
                }
            ]
        }
    ],
    "version": "2.0.0"
}

Y con esto iniciamos la compilación y posterior depuración con la tecla F5.