Chuyển tới nội dung chính

Trình biên dịch clang-cl

clang-cl là frontend C/C++ của LLVM được thiết kế để tương thích hoàn toàn với trình biên dịch MSVC (cl.exe). Nó cho phép bạn biên dịch các dự án Windows sử dụng cú pháp MSVC nhưng vẫn tận dụng được các công nghệ tối ưu và công cụ kiểm tra hiện đại từ hệ sinh thái LLVM như clangd, clang-tidy, lld-link, llvm-lib,...

Không giống như clang++ (dùng cú pháp kiểu UNIX/GNU), clang-cl sử dụng cú pháp kiểu MSVC, ví dụ: /c, /Fo, /link, /I, /D,...

clang-cl có thể biên dịch trực tiếp các project dùng .vcxproj, hoặc dùng độc lập trên dòng lệnh.

Cú pháp tổng quát

clang-cl [file nguồn] [cờ tiền xử lý] [cờ cấu hình biên dịch] [cờ cảnh báo] [cờ tối ưu hóa] [cờ include] [cờ đầu ra] [/link [cờ liên kết]]
Gợi ý thứ tự
  • [file nguồn] đặt ở đầu để dễ đọc
  • [cờ liên kết] (sau /link) nên luôn được đặt cuối cùng, vì mọi thứ sau /link sẽ được chuyển nguyên văn cho linker (lld-link hoặc link.exe). Việc đặt sai vị trí có thể khiến một số cờ bị bỏ qua hoặc bị hiểu sai.
  • Nhóm cờ được nhóm theo chức năng, giúp dễ debug và mở rộng
  • Có thể thay đổi thứ tự, nhưng khuyến nghị theo nhóm rõ ràng

Các nhóm cờ và giải thích

Dưới đây sẽ liệt kê các cờ cơ bản và phổ biến đại diện cho mỗi nhóm cờ trong cú pháp tổng quát.

  1. Cờ tiền xử lý
    • /D<macro>: Định nghĩa macro (giống #define). Ví dụ: /DDEBUG
    • /U<macro>: Xóa macro đã định nghĩa
    • /FI<file>: Yêu cầu trình biên dịch tự động include một file header (.h/.hpp) vào đầu mỗi file nguồn trong quá trình tiền xử lý
    • /E: Chạy tiền xử lý, in kết quả ra stdout
    • /showIncludes: Hiển thị danh sách các file header đã include

  2. Cờ cấu hình biên dịch
    • /c: Chỉ biên dịch, không liên kết
    • /LD: Tạo thư viện động .dll
    • /MD, /MT: Liên kết thư viện runtime động (/MD) hoặc tĩnh (/MT)
    • /EHsc: Bật xử lý exception kiểu C++
    • /GR-: Tắt RTTI
    • /m32//m64: Chỉ định kiến trúc 32-bit hoặc 64-bit khi biên dịch
    • /Zc:__cplusplus: Đảm bảo macro __cplusplus phản ánh đúng chuẩn

  3. Cờ cảnh báo
    • /W3, /W4, /Wall: Mức độ cảnh báo (tăng dần)
    • /WX: Cảnh báo được xem là lỗi
    • /permissive-: Tuân thủ chặt chẽ chuẩn C++ (tắt các extension của MSVC)
    • /std:c++20: Chỉ định chuẩn C++
    • /experimental:module: Bật hỗ trợ C++20 module (hỗ trợ C++20 module vẫn đang trong giai đoạn thử nghiệm và có thể không ổn định trên tất cả các phiên bản clang-cl)

  4. Cờ tối ưu hóa
    • /Od: Không tối ưu hóa
    • /O1, /O2, /Ox: Tối ưu mã ở các mức khác nhau
    • /GL: Bật Link-Time Optimization (LTO)
    • /Gw: Tối ưu hóa lưu trữ dữ liệu tĩnh (global)
    • /GS-: Tắt stack security check (không khuyến nghị trong production)

  5. Cờ include
    • /I<dir>: Thêm thư mục tìm kiếm header
    • /external:I<dir>: Include header kiểu “external” (dùng cho phân tích static)
    • /external:W0: Tắt cảnh báo từ external header

  6. Cờ đầu ra
    • /Fe:<file.exe>: Đặt tên file thực thi đầu ra (nếu không dùng /link /OUT:)
    • /Fo:<file.obj>: Đặt tên file đối tượng .obj đầu ra
    • /LD: Tạo thư viện động .dll
    • /LDd: Tạo .dll debug (kèm theo /MDd,... trong runtime)
    • /link /OUT:<file.exe>: Tùy chọn thay thế /Fe, truyền trực tiếp cho linker
    • /Zi: Sinh thông tin debug (PDB) để dùng với trình gỡ lỗi như VS hoặc WinDbg
    • /Z7: Ghi thông tin debug vào .obj thay vì file .pdb
    • /MP: Biên dịch nhiều file song song (multi-processing build), hiệu quả của cờ này phụ thuộc vào số lượng core CPU và kích thước dự án.
    • /showIncludes: In ra các file đã include (giúp tạo file dependency)

  7. Cờ liên kết (dùng sau /link)
    • /OUT:<file.exe>: Tên file đầu ra
    • /IMPLIB:<file.lib>: Tạo import library khi build .dll
    • /DLL: Tạo .dll (giống /LD)
    • /LIBPATH:<dir>: Chỉ định thư mục chứa .lib
    • /SUBSYSTEM:<arg>: Chỉ định loại chương trình: console, windows, native,... — ảnh hưởng đến entry point và hiển thị CMD.
    • kernel32.lib, user32.lib: Liên kết thủ công thư viện hệ thống

thông tin
  • clang-cl không dùng các cờ GNU-style như -std=c++20, -Wall, -g — mà thay bằng /std:c++20, /W4, /Zi
  • Dù là LLVM backend, nhưng toàn bộ interface phía trước tương tự MSVC (cl.exe)
  • Có thể kết hợp với lld-link hoặc link.exe
  • Thư viện chuẩn mặc định là MSVC STL, không phải libstdc++ hoặc libc++
  • Không hỗ trợ -stdlib=libc++, muốn dùng libc++ phải chuyển sang clang++ hoặc dùng cross-compilation
  • clang-cl hỗ trợ hầu hết các cờ của cl và hỗ trợ một số cờ của Clang, nhưng phải dùng cú pháp - (không đổi thành /). Để kiểm tra cờ Clang nào được hỗ trợ, bạn có thể chạy clang-cl -help hoặc thử nghiệm từng cờ cụ thể.
  • Một số cờ bắt buộc có ":" (/Zc:__cplusplus, /experimental:module,...), một số cờ bắt buộc không có ":" (/FI<file>, /Tc, /Tp,...) và một số cờ chấp nhận cách viết có hoặc không có ":" (/Fe:<file.exe>, /Fo:<file.obj>,...). Với nhóm cờ hỗ trợ cả hai cách viết có hoặc không có ":", khuyến khích nên chọn cú pháp dạng có ":" để tường minh và nhất quán. Ví dụ các cờ có cú pháp sau đều được chấp nhận: /Fe:file_out.exe, /Fefile_out.exe, /LIBPATH:lib, /LIBPATHlib.
  • Luôn kiểm tra tài liệu Microsoft (Compiler Options) hoặc clang-cl /? để xác nhận cú pháp đúng trước khi sử dụng.
  • clang-cl cũng chấp nhận kiểu viết /D<macro>/D <macro>, nhưng cách dùng không có khoảng trắng /D<macro> được khuyến khích.
ghi chú

Lựa chọn cờ đầu ra: /Fe: hay /link /OUT:?

  • Nên ưu tiên dùng /Fo:<file.obj>/Fe:<file.exe> để đặt tên file đầu ra ngay trong trình biên dịch clang-cl
  • Tránh dùng /link /OUT: trừ khi bạn cần điều khiển linker rất chi tiết (export symbol, PDB, DEF file...)
  • Các cờ như /Fe:, /Fo: tích hợp tốt với các cờ debug hoặc chia nhỏ build theo file
  • Sử dụng /Fe: rõ ràng hơn, dễ dùng hơn và là best practice khi biên dịch theo phong cách MSVC

Trình liên kết của clang++

  • Mặc định clang-cl sử dụng lld-link.exe, một linker của LLVM tương thích với link.exe của MSVC.
  • Có thể thay bằng link.exe từ MSVC nếu cần.
  • lld-link hỗ trợ LTO (/GL + /LTCG) và có tốc độ nhanh hơn link.exe trong nhiều dự án lớn.
  • Các cờ liên kết được đặt sau /link, ví dụ:
    clang-cl main.cpp /Fe:myapp.exe /link /LIBPATH:lib mylib.lib
  • Import library (.lib) nên được đặt sau /link, cùng với các thư viện hệ thống nếu cần (ví dụ: kernel32.lib, user32.lib)

Ví dụ

Biên dịch file thực thi .exe:

clang-cl src\main.cpp src\lib.cpp /std:c++20 /W4 /O2 /Iinclude /Fe:build\myapp.exe /link /LIBPATH:lib mylib.lib

Giải thích:

  • File nguồn: src\main.cpp, src\lib.cpp
  • Cờ cấu hình: /std:c++20
  • Cờ cảnh báo: /W4
  • Cờ tối ưu: /O2
  • Cờ include: /Iinclude
  • Cờ đầu ra: /Fe:build\myapp.exe
  • Cờ liên kết: /link /LIBPATH:lib mylib.lib

Biên dịch và sử dụng thư viện tĩnh (.lib)

1. Biên dịch mã nguồn thành file đối tượng (.obj)

  • Mục đích: Biên dịch các file mã nguồn (.cpp) thành file đối tượng (.obj) mà không liên kết, để chuẩn bị đóng gói thành thư viện .lib.

  • Cú pháp tổng quát:

    clang-cl /c <file.cpp> /Fo:<file.obj> [cờ biên dịch khác]
  • Ví dụ áp dụng cơ bản:

    clang-cl /c mylib.cpp /Fo:mylib.obj
    • /c: Chỉ biên dịch, không liên kết.
    • /Fo: Đặt tên file đối tượng đầu ra .obj.

    lưu ý
    • Có thể truyền nhiều file .cpp cùng lúc nếu muốn tạo nhiều .obj.
    • Đảm bảo file .cpp include đúng .h tương ứng để kiểm tra prototype.

2. Đóng gói file đối tượng thành thư viện tĩnh (.lib)

  • Mục đích: Gộp một hoặc nhiều file .obj thành một thư viện .lib để tái sử dụng và liên kết với các chương trình khác mà không cần biên dịch lại.

  • Cú pháp tổng quát:

    llvm-lib /out:<libname.lib> <file1.obj> [file2.obj ...]
  • Ví dụ áp dụng cơ bản:

    llvm-lib /out:mylib.lib mylib.obj
    • llvm-lib: Công cụ đóng gói thư viện trong hệ LLVM (tương tự lib.exe của MSVC).
    • /out:: Đặt tên file thư viện đầu ra.
    lưu ý
    • Nên sử dụng llvm-lib thay vì lib.exe để đồng bộ với clang-cl.
    • Có thể đóng gói nhiều .obj cùng lúc nếu thư viện có nhiều thành phần.
    • Không cần đặt tiền tố lib như với .a, nhưng nên đặt tên rõ ràng, ví dụ: mylib.lib.
  • Kiểm tra thư viện sau khi đóng gói:
    Sau khi tạo thư viện .lib từ .obj, bạn có thể kiểm tra nội dung bên trong để đảm bảo các symbol (hàm, biến) đã được đóng gói chính xác.

    • Dùng llvm-nm:
      llvm-nm mylib.lib
      • Liệt kê các symbol trong thư viện.
      • Dấu T (text/code), D (data) cho biết symbol được export.
    • Dùng dumpbin của Visual Studio (nếu có):
      dumpbin /symbols mylib.lib
      • Cung cấp thông tin rất chi tiết về tất cả symbol (có hoặc không export).
      • Chạy trong Developer Command Prompt của MSVC.
    • Không dùng llvm-ar hoặc ar:
      • .lib không tuân theo định dạng ar của UNIX, nên không nên dùng llvm-ar với .lib.
    thông tin
    • llvm-nm hoạt động tốt với .lib dạng COFF, nếu được tạo bởi clang-cl hoặc llvm-lib.
    • Nếu llvm-nm không hiển thị gì, hãy thử dumpbin để kiểm tra sâu hơn.
    • Kiểm tra symbol đặc biệt hữu ích nếu bạn viết thư viện cho bên thứ ba hoặc cần xác minh symbol có đúng kiểu gọi hàm (__cdecl, __stdcall) và có bị name mangling hay không.

3. Liên kết thư viện .lib với chương trình chính

  • Mục đích: Biên dịch chương trình chính (.cpp) và liên kết với mylib.lib để tạo file thực thi.

  • Cú pháp tổng quát:

    clang-cl <main.cpp> [cờ biên dịch khác] /I<dir_include> /Fe:<output.exe> /link /LIBPATH:<dir_lib> <libname.lib>
  • Ví dụ áp dụng cơ bản:

    clang-cl main.cpp /Iinclude /Fe:myapp.exe /link /LIBPATH:lib mylib.lib
    • main.cpp: File mã nguồn chính, gọi hàm từ mylib.h.
    • /Iinclude: Chỉ định thư mục chứa file header.
    • /Fe: Tên file thực thi đầu ra.
    • /link /LIBPATH:lib mylib.lib: Chỉ định thư viện liên kết.
    lưu ý
    • Không dùng -l<name> như trong clang++; với clang-cl, chỉ cần ghi trực tiếp tên file .lib.
    • mylib.lib phải được tạo bằng llvm-lib hoặc lib.exe để đảm bảo tương thích.
    • Cờ /LIBPATH: bắt buộc nếu .lib không nằm trong thư mục hiện tại.

Biên dịch và sử dụng thư viện động (.dll)

Thư viện động .dll (Dynamic-Link Library) trong môi trường Windows cho phép nhiều chương trình chia sẻ chung mã thực thi, giúp giảm dung lượng file .exe và hỗ trợ nạp thư viện động tại runtime. Có hai phương pháp sử dụng:

  • Liên kết tĩnh (implicit linking): Gọi hàm từ .dll thông qua file import library .lib.
  • Tải động (explicit linking): Nạp .dll thủ công trong runtime bằng LoadLibrary / GetProcAddress.

Cả hai cách đều chỉ yêu cầu phân phối .exe.dll. File .lib chỉ cần trong giai đoạn biên dịch (implicit linking).

1. Biên dịch mã nguồn thành file đối tượng (.obj)

  • Mục đích: Tạo file .obj từ mã nguồn .cpp, để sau đó build .dll.
  • Cú pháp tổng quát:
    clang-cl /c <file.cpp> /Fo:<file.obj> [cờ biên dịch khác]
  • Ví dụ áp dụng cơ bản:
    clang-cl /c src\mylib.cpp /Fo:build\mylib.obj /DBUILD_MYLIB /Iinclude
    • /DBUILD_MYLIB: Macro để export symbol.
    • /Iinclude: Thư mục chứa mylib.h.
  • File header:
    • Dùng __declspec(dllexport)__declspec(dllimport) thông qua macro để khai báo symbol dùng chung:
      #pragma once

      #ifdef BUILD_MYLIB
      #define MYLIB_API __declspec(dllexport)
      #else
      #define MYLIB_API __declspec(dllimport)
      #endif

      extern "C" {
      MYLIB_API int add(int a, int b);
      }
    • Lưu ý:
      • extern "C" giúp tránh name mangling, cần thiết nếu .dll sẽ được gọi từ ngôn ngữ khác (C, Python, AutoIt,...).
      • Không nên sử dụng extern "C" với các thành phần đặc trưng của C++ như std::string, std::vector, hàm overload, hàm template, class hoặc namespace. Những thành phần này không tương thích với ngữ cảnh extern "C" vì không có định dạng ABI (Application Binary Interface) theo chuẩn C, có thể dẫn đến lỗi biên dịch hoặc lỗi liên kết (linker error).
Calling convention là gì?
  1. Calling convention là gì?
    Calling convention (quy ước gọi hàm) là quy tắc quy định:

    • Thứ tự truyền tham số
    • Ai chịu trách nhiệm thu dọn stack (caller hay callee)
    • Cách đặt tên hàm (name mangling) (nếu có)
  2. Các loại phổ biến trong Windows (x86)

    Tên gọiTừ khóaTruyền tham sốThu dọn stackDùng phổ biến ở đâu
    Cdecl__cdeclTừ phải sang tráiCallerMặc định trong C/C++
    Stdcall__stdcallTừ phải sang tráiCalleeWindows API, AutoIt, VB6
    Fastcall__fastcallMột số dùng thanh ghiCalleeHiệu năng cao (ít dùng)
    Thiscall__thiscallthis trong ecx, còn lại trên stackCalleeMặc định với hàm thành viên C++

    a. __cdecl

    • Caller (người gọi) phải clean stack sau khi gọi hàm.
    • Cho phép overload số lượng tham số (printf, scanf)
    • Thường dùng mặc định trong C/C++ compile
      __cdecl int add(int a, int b);

    b. __stdcall

    • Callee (hàm được gọi) sẽ tự động dọn stack.
    • Thường dùng cho Windows API, DLL để gọi từ ngôn ngữ khác
    • Gọi từ AutoIt, VB6, Pascal, C# (P/Invoke) → nên dùng __stdcall
      __stdcall int add(int a, int b);  // gọn hơn cho người gọi

    c. __fastcall

    • Truyền 2 tham số đầu tiên qua thanh ghi (ecx, edx) → nhanh hơn.
    • Stack dọn bởi callee.
    • Thường không cần thiết trừ khi tối ưu hóa chuyên sâu.
      __fastcall int add(int a, int b);

    d. __thiscall (mặc định cho C++ member function)

    • this nằm trong thanh ghi ecx
    • Các tham số còn lại trên stack
    • Không dùng được trong extern "C" (vì C không có this)

Nếu không khai báo rõ __stdcall, thì hàm export từ DLL sẽ dùng calling convention mặc định, thường là:

  • __cdecl trên Windows x86
  • Trên Windows x64: calling convention mặc định là Microsoft x64 ABI, không cần khai báo (vì tất cả dùng chung)

TÌNH HUỐNG:

  • Nếu đang dùng Windows 64-bit:
    • Không có __stdcall hay __cdecl phân biệt nữa
    • Tất cả hàm gọi giữa module đều theo x64 calling convention (được thống nhất)
    • Chương trình khác (64-bit) vẫn gọi được hàm dù không khai báo __stdcall
  • Nhưng nếu viết DLL 32-bit và chương trình khác (32-bit) sử dụng DLL này:
    • Thì phải chỉ định rõ __stdcall; Nếu không, sẽ xảy ra:
      • Lỗi stack khi trả về từ hàm
      • Treo chương trình hoặc kết quả sai

2. Tạo thư viện động (.dll) và import library (.lib)

  • Mục đích: Liên kết file .obj để tạo mylib.dll và file mylib.lib để dùng khi implicit linking.
  • Cú pháp tổng quát:
    clang-cl /LD <file.obj> [/I<dir>] [/D<macro>] /Fe:<file.dll> [cờ biên dịch khác] /link /IMPLIB:<file.lib> [/DEF:<file.def>] [/LIBPATH:<dir>] [lib1.lib ...] [/DEBUG] [cờ linker khác]
  • Ví dụ áp dụng cơ bản:
    clang-cl /LD build\mylib.obj /Fe:bin\mylib.dll /link /IMPLIB:lib\mylib.lib
    • /LD: Biên dịch và liên kết thành .dll.
    • /Fe: Tên file .dll đầu ra.
    • /IMPLIB:: Tạo file .lib chứa thông tin liên kết (import library).
  • Cấu trúc tổ chức dự án mẫu:
    project/
    ├── include/mylib.h
    ├── src/mylib.cpp
    ├── build/mylib.obj
    ├── lib/mylib.lib
    ├── bin/mylib.dll
  • Lưu ý:
    • Có thể thêm /DEF:mylib.def nếu bạn muốn kiểm soát chính xác symbol export. Nội dung mylib.def ví dụ:
      LIBRARY "mylib"
      EXPORTS
      add
      • LIBRARY là tên DLL (không cần đuôi .dll)
      • EXPORTS liệt kê các hàm public
    • Nếu chỉ dùng để tải động .dll, có thể bỏ /IMPLIB, ví dụ:
      clang-cl /LD build\mylib.obj /Fe:bin\mylib.dll

3. Sử dụng thư viện động .dll

  1. Liên kết tĩnh (Implicit linking)
    • Mục đích: Gọi hàm như thông thường qua .lib, nhưng thật ra lời gọi được định tuyến tới .dll.
    • Cú pháp tổng quát:
      clang-cl <main.cpp> [cờ biên dịch khác] /I<dir_include> /Fe:<output.exe> /link /LIBPATH:<dir_lib> <libname.lib>
    • Ví dụ áp dụng cơ bản:
      clang-cl main.cpp /Iinclude /Fe:myapp.exe /link /LIBPATH:lib mylib.lib
      • main.cpp: File nguồn, gọi các hàm được khai báo trong mylib.h.
      • /Iinclude: Khai báo thư mục chứa file mylib.h.
      • /Fe:myapp.exe: File thực thi đầu ra.
      • /link /LIBPATH:lib mylib.lib: Chỉ định thư mục chứa file .lib và tên của file .lib (mylib.lib) để liên kết.
    • Lưu ý:
      • Cần mylib.h với __declspec(dllimport) (macro MYLIB_API) để khai báo symbol.
      • mylib.dll phải có mặt khi chạy chương trình:
        • Đặt cùng thư mục với .exe, hoặc
        • Thêm thư mục chứa .dll vào PATH, hoặc
        • Dùng API như SetDllDirectory() để thiết lập.
  2. Tải động (Explicit linking)
    • Mục đích: Nạp .dll tại runtime bằng API LoadLibrary, không cần .lib, linh hoạt hơn.
    • Ví dụ:
      #include <windows.h>
      #include <iostream>

      typedef int (*AddFunc)(int, int);

      int main() {
      HMODULE hDLL = LoadLibraryA("mylib.dll");
      if (!hDLL) {
      std::cerr << "Không thể nạp mylib.dll\n";
      return 1;
      }

      AddFunc add = (AddFunc)GetProcAddress(hDLL, "add");
      if (!add) {
      std::cerr << "Không tìm thấy hàm add\n";
      return 1;
      }

      std::cout << "2 + 3 = " << add(2, 3) << "\n";
      FreeLibrary(hDLL);
      return 0;
      }
    • Biên dịch chương trình:
      clang-cl src\main.cpp /Fe:bin\myapp.exe
    • Lưu ý:
      • Không cần mylib.lib hoặc mylib.h, nhưng cần:
        • Tên hàm chính xác (add)
        • Prototype (kiểu đối số, trả về)
        • Biết chuẩn gọi hàm (__cdecl mặc định)
      • Có thể dùng llvm-nm hoặc dumpbin /exports để xem các hàm export trong .dll.