c++cmakecudaclangclang++

nvlink error for C++ virtual classes when compiling CUDA with clang


When working on a CUDA project I get an nvlink error stating that __cxa_pure_virtual has undefned reference. My repository is structured like the example in ModernCMake - Extended example.

Surprisingly I don't get this error when compiling with nvcc, I want to compile my code using clang because I want to work with clang's LSP.

This is probably due to the way I'm handling virtual classes. Here is my compile command with the related error:

➜  CUDA-Path-Tracer git:(main) ✗ make build
cmake -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_CUDA_COMPILER=clang++-20 -DCMAKE_CXX_COMPILER=clang++-20 -DCMAKE_CUDA_ARCHITECTURES=native
-- The CXX compiler identification is Clang 20.0.0
-- The CUDA compiler identification is Clang 20.0.0
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/clang++-20 - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Detecting CUDA compiler ABI info
-- Detecting CUDA compiler ABI info - done
-- Check for working CUDA compiler: /usr/bin/clang++-20 - skipped
-- Detecting CUDA compile features
-- Detecting CUDA compile features - done
-- Performing Test HAVE_FLAG__ffile_prefix_map__home_eduard_Git_CUDA_Path_Tracer_build__deps_catch2_src__
-- Performing Test HAVE_FLAG__ffile_prefix_map__home_eduard_Git_CUDA_Path_Tracer_build__deps_catch2_src__ - Success
-- Configuring done (14.8s)
-- Generating done (0.1s)
-- Build files have been written to: /home/eduard/Git/CUDA-Path-Tracer/build
cmake --build build
gmake[1]: Entering directory '/home/eduard/Git/CUDA-Path-Tracer/build'
gmake[2]: Entering directory '/home/eduard/Git/CUDA-Path-Tracer/build'
gmake[3]: Entering directory '/home/eduard/Git/CUDA-Path-Tracer/build'
gmake[3]: Leaving directory '/home/eduard/Git/CUDA-Path-Tracer/build'
gmake[3]: Entering directory '/home/eduard/Git/CUDA-Path-Tracer/build'
[  0%] Building CUDA object src/CMakeFiles/cuda_path_tracer_lib.dir/error.cu.o
clang++-20: warning: CUDA version 12.6 is only partially supported [-Wunknown-cuda-version]
[  0%] Building CUDA object src/CMakeFiles/cuda_path_tracer_lib.dir/vec3.cu.o
clang++-20: warning: CUDA version 12.6 is only partially supported [-Wunknown-cuda-version]
[  1%] Building CUDA object src/CMakeFiles/cuda_path_tracer_lib.dir/sphere.cu.o
clang++-20: warning: CUDA version 12.6 is only partially supported [-Wunknown-cuda-version]
[  2%] Building CUDA object src/CMakeFiles/cuda_path_tracer_lib.dir/render.cu.o
clang++-20: warning: CUDA version 12.6 is only partially supported [-Wunknown-cuda-version]
[  3%] Building CUDA object src/CMakeFiles/cuda_path_tracer_lib.dir/world.cu.o
clang++-20: warning: CUDA version 12.6 is only partially supported [-Wunknown-cuda-version]
[  4%] Building CUDA object src/CMakeFiles/cuda_path_tracer_lib.dir/ray.cu.o
clang++-20: warning: CUDA version 12.6 is only partially supported [-Wunknown-cuda-version]
[  5%] Building CXX object src/CMakeFiles/cuda_path_tracer_lib.dir/image.cpp.o
nvlink error   : Undefined reference to '__cxa_pure_virtual' in 'src/CMakeFiles/cuda_path_tracer_lib.dir/sphere.cu.o'
gmake[3]: *** [src/CMakeFiles/cuda_path_tracer_lib.dir/build.make:177: src/CMakeFiles/cuda_path_tracer_lib.dir/sm_90.cubin] Error 255
gmake[3]: Leaving directory '/home/eduard/Git/CUDA-Path-Tracer/build'
gmake[2]: *** [CMakeFiles/Makefile2:924: src/CMakeFiles/cuda_path_tracer_lib.dir/all] Error 2
gmake[2]: Leaving directory '/home/eduard/Git/CUDA-Path-Tracer/build'
gmake[1]: *** [Makefile:101: all] Error 2
gmake[1]: Leaving directory '/home/eduard/Git/CUDA-Path-Tracer/build'
make: *** [Makefile:12: build] Error 2

My repository atm is structured in this way:

├── apps
│   ├── CMakeLists.txt
│   └── main.cpp
├── CMakeLists.txt
├── include
│   └── cuda_path_tracer
│       ├── error.cuh
│       ├── image.hpp
│       ├── ray.cuh
│       ├── render.cuh
│       ├── shape.cuh
│       ├── sphere.cuh
│       ├── vec3.cuh
│       └── world.cuh
├── LICENSE
├── llvm.sh
├── Makefile
├── README.md
├── report
│   └── report.typ
├── src
│   ├── CMakeLists.txt
│   ├── error.cu
│   ├── image.cpp
│   ├── ray.cu
│   ├── render.cu
│   ├── sphere.cu
│   ├── vec3.cu
│   └── world.cu
└── tests
    ├── CMakeLists.txt
    ├── test_error.cpp
    ├── test_image.cpp
    ├── test_ray.cpp
    └── test_vec3.cpp

The only virtual class I have is Shape which is defined as

/**
 * @file shape.cuh
 */

#pragma once

#include "cuda_path_tracer/ray.cuh"

class Shape {
public:
  __host__ __device__ virtual ~Shape() = default;
  __host__ __device__ virtual auto hit(const Ray &r) const -> bool = 0;
};

with the corresponding Sphere class being

/**
 * @file sphere.cuh
 */

#pragma once

#include "shape.cuh"

class Sphere : public Shape {
public:
  __host__ __device__ Sphere(const Vec3 &center, float radius);
  __host__ __device__ auto hit(const Ray &r) const -> bool override;

private:
  Vec3 center;
  float radius;
};

and

/**
 * @file sphere.cu
 */

#include "cuda_path_tracer/sphere.cuh"

__host__ __device__ Sphere::Sphere(const Vec3 &center, float radius)
    : center(center), radius(radius) {}

__host__ __device__ auto Sphere::hit(const Ray &r) const -> bool {
  Vec3 oc = r.getOrigin() - center;
  float a = r.getDirection().dot(r.getDirection());
  float b = 2.0f * oc.dot(r.getDirection());
  float c = oc.dot(oc) - radius * radius;
  float discriminant = b * b - 4 * a * c;
  return discriminant > 0;
}

I have tried different versions of the llvm suite, specifically v18, 19 and now 20. I have tried tried defining implicit copy/move members as suggested by clang-tidy, I have tried defining a default constructor in shape.cuh but to no avail.

I can confirm that the problem stems from these classes because the code compiles when their code is commented out.

I have also tried defining the __cxa_pure_virtual function as a loop as suggested in some issues (while(1) {}) but that also did not work or perhaps I defined it in the wrong header.


Solution

  • Turns out the solution was indeed to define the __cxa_pure_virtual as a dummy function due to https://bugs.llvm.org/show_bug.cgi?id=49839 , the issue is that I wasn't adding the __device__ attribute at the front. The function should then be something like:

    extern "C" __device__ void __cxa_pure_virtual() {
      while (1) {
      }
    }