cwindowsgmp

Compiling C Library Using GMP on Windows


I'm not a programmer, just writing some code for personal use from time to time, so I don't have much experience and knowledge. I'm using CMake and gcc/g++ on Linux or clang on Android (in Termux). Since, I only used those compilers (which have much the same flags), I wrote CMake code to accept only them (I noticed that MSVC has different flags, so, I just didn't spend my time on it).

Recently, I wrote a dynamic C library which uses the GMP library (GMP is linked statically, so, the library can be used on another desktop, without need of installing GMP). But there was a need to compile it on Windows, so, my mate could use the it too. I spent 2 days reading all information I've found and trying to compile it, but without luck. I tried using Visual Studio 2022 and Visual Studio Code with the MSVC compiler but it looked like a completely different workflow to me. I tried using vcpkg but still wasn't able to compile the library (I just aborted the compilation process after waiting more than 1 hour… On Linux it compiled for less than 10 seconds). Switching to another library, like MPIR, didn't look good to me (don't want to rewrite the existing code).

Finally, I managed to compile the DLL library for Windows. Below, I'll provide some initial (Linux) dummy code (for the sake of simplicity), the steps that I took to compile the library, and describe what I changed for Windows.

I'm posting this for the following reasons:

  1. I hope that the information will useful for others who will struggle with similar problems.
  2. It'd be great to get other solutions, recommendations, etc. E.g., how to make the code and process of compilation of static/dynamic libraries cross-platform?

Update
Moved the solution I came up with (not the best, I think) to the answers.


Solution

  • Example code and steps of my solution are below.

    Project structure:

    dll
    ├src
    │├test
    ││├test.h
    ││└test.c
    │├main.c
    │└main.py
    ├build.sh
    └CMakeLists.txt
    

    Initial project files (for Linux)
    test.h:

    #ifndef TEST_H
    #define TEST_H
    
    #include <gmp.h>
    
    void Double(mpz_t a);
    void Triple(mpz_t a);
    
    #endif  // TEST_H
    

    test.c:

    #include "test.h"
    
    #include <gmp.h>
    
    void Double(mpz_t a) { mpz_mul_ui(a, a, 2UL); }
    void Triple(mpz_t a) { mpz_mul_ui(a, a, 3UL); }
    
    

    main.c:

    #include <stdbool.h>
    #include <stdio.h>
    
    #include "test/test.h"
    
    #include <gmp.h>
    
    bool Mult(const char* const val, const char op) {
      mpz_t num;
      mpz_init_set_str(num, val, 0);
      gmp_printf(" In: %Zi\n", num);
      if (op == 'd') { Double(num); }
      else if (op == 't') { Triple(num); }
      else {
        printf("Invalid argument!\n");
        mpz_clear(num);
        return false;
      }
      gmp_printf("Out: %Zi\n", num);
      mpz_clear(num);
      return true;
    }
    

    main.py:

    import ctypes
    import os
    import platform
    
    def GetFunction():
      osys = platform.system()
      lib_path = r'/home/linux/Documents/tests/dll/cmake_build'
      lib_path = os.path.join(lib_path, 'libtest.')
      if osys == 'Windows': lib_path += 'dll'
      else: lib_path += 'so'
      lib = ctypes.CDLL(lib_path)
      lib.Mult.restype = ctypes.c_bool
      lib.Mult.argtypes = [ctypes.c_char_p, ctypes.c_char]
      return lib.Mult
    
    if __name__ == '__main__':
      mult = GetFunction()
      big_num = 12345678901234567890
      big_num = f'{big_num}'.encode('ascii')
      op = 't'.encode('ascii')
      mult(big_num, op)
    

    build.sh:

    #!/bin/bash
    
    n_args=$#
    if [ $n_args -gt 0 ]; then
      echo "Illegal number of parameters!"
      exit 1
    fi
    
    BLD='cmake_build'
    BIN='libtest.so'
    
    set -e  # Exit on error.
    
    [ -d "$BLD" ] || mkdir "$BLD"  # Create build dir.
    cd "$BLD"
    [ -f "$BIN" ] && rm "$BIN"  # Delete compiled file.
    
    # Build and run.
    cmake -DCMAKE_BUILD_TYPE=Release ..
    cmake --build .
    
    cd ..
    

    CmakeLists.txt:

    cmake_minimum_required(VERSION 3.10)
    set(pref "lib_")  # Prefix.
    project(${pref}test)
    set(CMAKE_C_STANDARD 17)
    set(CMAKE_C_STANDARD_REQUIRED ON)
    set(CMAKE_C_EXTENSIONS OFF)
    
    string(LENGTH ${pref} len)
    string(SUBSTRING ${PROJECT_NAME} ${len} -1 out_name)
    string(REPLACE "_" "" out_name ${out_name})
    string(REPLACE "_" "" lib_name ${PROJECT_NAME})
    unset(len)
    
    string(TOLOWER "${CMAKE_BUILD_TYPE}" conf_build_type)
    string(TOLOWER "Debug" conf_debug)
    string(TOLOWER "Release" conf_release)
    if(NOT conf_build_type MATCHES "^(${conf_debug}|${conf_release})$")
      message(FATAL_ERROR "Unknown build configuration (\"${CMAKE_BUILD_TYPE}\")!")
    endif()
    
    set(comp_gnu "GNU")
    set(comp_clang "Clang")
    if(CMAKE_C_COMPILER_ID STREQUAL comp_gnu)
      set(COMPILER_VERSION_REQD "13.0.0")
    elseif(CMAKE_C_COMPILER_ID STREQUAL comp_clang)
      set(COMPILER_VERSION_REQD "17.0.0")
    else()
      message(FATAL_ERROR "\tNot supported compiler (${CMAKE_C_COMPILER_ID})!")
    endif()
    if(CMAKE_C_COMPILER_VERSION VERSION_LESS COMPILER_VERSION_REQD)
      message(FATAL_ERROR "Version ${COMPILER_VERSION_REQD} or higher required!")
    endif()
    
    message("\t${CMAKE_C_COMPILER_ID} ${CMAKE_C_COMPILER_VERSION} "
        "${CMAKE_BUILD_TYPE} ${lib_name}")
    
    string(CONCAT flags "-W;-Wall;-Wextra;-Wpedantic;-Wconversion;"
        "-Werror=return-type;-Wsign-conversion")
    if(CONF_BUILD_TYPE STREQUAL conf_debug)
      string(CONCAT flags "${flags}" ";-O0;-g")
      string(CONCAT flags "${flags}" ";-fsanitize=address")
      add_compile_options("$<$<CONFIG:DEBUG>:${flags}>")
      add_link_options("$<$<CONFIG:DEBUG>:-fsanitize=address>")
    elseif(CONF_BUILD_TYPE STREQUAL conf_release)
      string(CONCAT flags "${flags}" ";-Ofast")
      add_compile_options("$<$<CONFIG:RELEASE>:${flags}>")
    endif()
    
    set(proj_fpath "${CMAKE_CURRENT_SOURCE_DIR}/")
    set(exec_path "${proj_fpath}src/")
    add_library(${lib_name} SHARED
        "${exec_path}main.c"
        "${exec_path}test/test.c")
    target_include_directories(${lib_name} PRIVATE "${exec_path}")
    target_link_libraries(${lib_name} PRIVATE gmp)
    set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static")
    
    set_target_properties(${lib_name} PROPERTIES OUTPUT_NAME "${out_name}")
    

    Steps that helped to solve the problem

    Modified portions of project files (for Windows)
    main.c:

    ...
    #include <gmp.h>
    
    #ifdef _WIN32
      #define DLLEXPORT __declspec(dllexport)
    #else
      #define DLLEXPORT
    #endif  // _WIN32
    
    DLLEXPORT bool Mult(const char* const val, const char op) {
    ...
    

    main.py:

    ...
      osys = platform.system()
      lib_path = r'C:\Users\Linux\diff\dll\cmake_build'
      lib_path = os.path.join(lib_path, 'libtest.')
    ...
    

    build.sh:

    ...
    
    BLD='cmake_build'
    BIN='libtest.dll'
    
    set -e  # Exit on error.
    ...
    # Build and run.
    cmake -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release ..
    cmake --build .
    ...
    

    CmakeLists.txt:

    ...
    target_include_directories(${lib_name} PRIVATE "${exec_path}")
    
    #find_package(GMP REQUIRED)  # This line throws an error, so, I just commented it out.
    if(NOT GMP_FOUND)
      set(GMP_ROOT_DIR "C:/msys64/mingw64/")
      set(GMP_INCLUDE_DIRS "${GMP_ROOT_DIR}/include")
      set(GMP_LIBRARIES "${GMP_ROOT_DIR}/lib/libgmp.a")
      set(GMP_FOUND TRUE)
    endif()
    target_link_libraries(${lib_name} ${GMP_LIBRARIES})
    
    set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static")
    ...