Libraries support in clangd

Intro

Imagine you are building an app in C, and you want to use library X. You downloaded library X, you compiled library X and you installed library X. Library X code included in the app works, so you continue to build the app, to discover inside the IDE that not all IDE functionalities work, for example jumping to function implementation result in displaying only declaration inside header file.

Wouldn't it be nice if we could somehow tell the IDE where are source files of library X?

In fact we can, thanks to marvelous invention which is compile commands file.

Compile command file is concept taken from the clangd server, so if you are not using it, i recomend you to check it out.

compile_commands.json

The file named compile_commands.json is used by clangd to index source code files it's gonna handle, so later when you use your fancy IDE functions like jump to definition it can browse indexes db and find in which source file the appropriate function resides.

It's syntax is pretty straigtforward, it's a list of dicts containing compilation command, input file path and output file path:

[
  {
    "directory": "/home/taba1uga/app",
    "command": "gcc -I. -o main.o -c main.c",
    "file": "main.c",
    "output": "main.o"
  }
]

Let's go back to the example from intro, we have an library x which is created from two files lib.c exposing definition for function foo:

#include <stdio.h>

#include "lib.h"

int foo(void) {
  puts(__func__);
  return 0;
}

and lib.h exposing declaration of function foo:

#ifndef LIB_H
#define LIB_H

int foo(void);

#endif // LIB_H

We placed library x in our home directory. To compile the library we use following command:

❯ gcc -I. -o lib.o -c lib.c

If we were creating compile_commands.json by hand it may look sth like this:

[
  {
    "directory": "/home/taba1uga/x",
    "command": "gcc -I. -o lib.o -c lib.c",
    "file": "lib.c",
    "output": "lib.o"
  }
]

We have also an app which is composed of only one file main.c which executes function foo:

#include <lib.h>

int main(void) {
  foo();
  return 0;
}

To compile the app we use following command:

❯ gcc -I../x/ -o main.o -c main.c

The compile_commands.json file for app may look sth like this:

[
  {
    "directory": "/home/taba1uga/app",
    "command": "gcc -I../x/ -o main.o -c main.c",
    "file": "main.c",
    "output": "main.o"
  }
]

Now let's assume we started clangd server in the app directory and asked for foo function definition:

❯ clangd-19 --pretty --log=verbose --compile-commands-dir=/home/taba1uga/app/
I[15:09:25.572] Debian clangd version 19.1.7 (3+b1)
I[15:09:25.572] Features: linux+grpc
I[15:09:25.572] PID: 1649700
I[15:09:25.572] Working directory: /home/taba1uga/app/
I[15:09:25.572] argv[0]: clangd-19
I[15:09:25.572] argv[1]: --pretty
I[15:09:25.572] argv[2]: --log=verbose
I[15:09:25.572] argv[3]: --compile-commands-dir=/home/taba1uga/app/
V[15:09:25.572] User config file is /home/taba1uga/.config/clangd/config.yaml
I[15:09:25.572] Starting LSP over stdin/stdout
V[15:09:25.573] <<< {
  "id": 1,
  "jsonrpc": "2.0",
  "method": "initialize",
  "params": {
    "capabilities": {},
    "processId": 1649698,
    "rootUri": "file:///home/taba1uga/app"
  }
}

I[15:09:25.573] <-- initialize(1)
I[15:09:25.574] --> reply:initialize(1) 1 ms
V[15:09:25.574] >>> {
  "id": 1,
  "jsonrpc": "2.0",
  "result": {
    "capabilities": {
      "astProvider": true,
      "callHierarchyProvider": true,
      "clangdInlayHintsProvider": true,
      "codeActionProvider": true,
      "compilationDatabase": {
        "automaticReload": true
      },
      "completionProvider": {
        "resolveProvider": false,
        "triggerCharacters": [
          ".",
          "<",
          ">",
          ":",
          "\"",
          "/",
          "*"
        ]
      },
      "declarationProvider": true,
      "definitionProvider": true,
      "documentFormattingProvider": true,
      "documentHighlightProvider": true,
      "documentLinkProvider": {
        "resolveProvider": false
      },
      "documentOnTypeFormattingProvider": {
        "firstTriggerCharacter": "\n",
        "moreTriggerCharacter": []
      },
      "documentRangeFormattingProvider": true,
      "documentSymbolProvider": true,
      "executeCommandProvider": {
        "commands": [
          "clangd.applyFix",
          "clangd.applyRename",
          "clangd.applyTweak"
        ]
      },
      "foldingRangeProvider": true,
      "hoverProvider": true,
      "implementationProvider": true,
      "inactiveRegionsProvider": true,
      "inlayHintProvider": true,
      "memoryUsageProvider": true,
      "referencesProvider": true,
      "renameProvider": true,
      "selectionRangeProvider": true,
      "semanticTokensProvider": {
        "full": {
          "delta": true
        },
        "legend": {
          "tokenModifiers": [
            "declaration",
            "definition",
            "deprecated",
            "deduced",
            "readonly",
            "static",
            "abstract",
            "virtual",
            "dependentName",
            "defaultLibrary",
            "usedAsMutableReference",
            "usedAsMutablePointer",
            "constructorOrDestructor",
            "userDefined",
            "functionScope",
            "classScope",
            "fileScope",
            "globalScope"
          ],
          "tokenTypes": [
            "variable",
            "variable",
            "parameter",
            "function",
            "method",
            "function",
            "property",
            "variable",
            "class",
            "interface",
            "enum",
            "enumMember",
            "type",
            "type",
            "unknown",
            "namespace",
            "typeParameter",
            "concept",
            "type",
            "macro",
            "modifier",
            "operator",
            "bracket",
            "label",
            "comment"
          ]
        },
        "range": false
      },
      "signatureHelpProvider": {
        "triggerCharacters": [
          "(",
          ")",
          "{",
          "}",
          "<",
          ">",
          ","
        ]
      },
      "standardTypeHierarchyProvider": true,
      "textDocumentSync": {
        "change": 2,
        "openClose": true,
        "save": true
      },
      "typeDefinitionProvider": true,
      "typeHierarchyProvider": true,
      "workspaceSymbolProvider": true
    },
    "serverInfo": {
      "name": "clangd",
      "version": "Debian clangd version 19.1.7 (3+b1) linux+grpc x86_64-pc-linux-gnu"
    }
  }
}
V[15:09:27.518] ASTWorker running Highlights on version 1 of /home/taba1uga/app/main.c
I[15:09:27.519] --> reply:textDocument/documentHighlight(4) 0 ms
V[15:09:27.519] >>> {
  "id": 4,
  "jsonrpc": "2.0",
  "result": [
    {
      "kind": 1,
      "range": {
        "end": {
          "character": 5,
          "line": 3
        },
        "start": {
          "character": 2,
          "line": 3
        }
      }
    }
  ]
}

V[15:09:27.519] ASTWorker running Hover on version 1 of /home/taba1uga/app/main.c
I[15:09:27.520] --> reply:textDocument/hover(5) 2 ms
V[15:09:27.520] <<< {
  "id": 6,
  "jsonrpc": "2.0",
  "method": "textDocument/definition",
  "params": {
    "position": {
      "character": 3,
      "line": 3
    },
    "textDocument": {
      "uri": "file:///home/taba1uga/app/main.c"
    }
  }
}

I[15:09:27.521] <-- textDocument/definition(6)
V[15:09:27.521] >>> {
  "id": 5,
  "jsonrpc": "2.0",
  "result": {
    "contents": {
      "kind": "plaintext",
      "value": "function foo\nprovided by <lib.h>\n\n→ int\n\nint foo(void)"
    },
    "range": {
      "end": {
        "character": 5,
        "line": 3
      },
      "start": {
        "character": 2,
        "line": 3
      }
    }
  }
}

V[15:09:27.522] ASTWorker running Definitions on version 1 of /home/taba1uga/app/main.c
I[15:09:27.522] --> reply:textDocument/definition(6) 1 ms
V[15:09:27.522] >>> {
  "id": 6,
  "jsonrpc": "2.0",
  "result": [
    {
      "range": {
        "end": {
          "character": 7,
          "line": 3
        },
        "start": {
          "character": 4,
          "line": 3
        }
      },
      "uri": "file:///home/taba1uga/lib/lib.h"
    }
  ]
}

As you can see from the log, when we ask for defintion we are being redirected to lib.h file which is not exactly what we wanted.

So we need to tell clangd about the lib.c by adding it's entry to compile_commands.json in app's directory:

[
  {
    "directory": "/home/taba1uga/app",
    "command": "gcc -I../x/ -o main.o -c main.c",
    "file": "main.c",
    "output": "main.o"
  },
  {
    "directory": "/home/taba1uga/app",
    "command": "gcc -I../x/ -o ../x/lib.o -c ../x/lib.c",
    "file": "../x/lib.c",
    "output": "../x/lib.o"
  }
]

Let's run clangd and ask again for foo definition:

❯ clangd-19 --pretty --log=verbose --compile-commands-dir=/home/taba1uga/app/
I[15:14:46.198] Debian clangd version 19.1.7 (3+b1)
I[15:14:46.198] Features: linux+grpc
I[15:14:46.198] PID: 1657010
I[15:14:46.198] Working directory: /home/taba1uga/Github/learning_clangd/cc/app
I[15:14:46.198] argv[0]: clangd-19
I[15:14:46.198] argv[1]: --pretty
I[15:14:46.198] argv[2]: --log=verbose
I[15:14:46.198] argv[3]: --compile-commands-dir=/home/taba1uga/Github/learning_clangd/cc/app/
V[15:14:46.198] User config file is /home/taba1uga/.config/clangd/config.yaml
I[15:14:46.198] Starting LSP over stdin/stdout
V[15:14:46.198] <<< {
  "id": 1,
  "jsonrpc": "2.0",
  "method": "initialize",
  "params": {
    "capabilities": {},
    "processId": 1657008,
    "rootUri": "file:///home/taba1uga/Github/learning_clangd/cc/app"
  }
}

I[15:14:46.198] <-- initialize(1)
I[15:14:46.199] --> reply:initialize(1) 0 ms
V[15:14:46.199] >>> {
  "id": 1,
  "jsonrpc": "2.0",
  "result": {
    "capabilities": {
      "astProvider": true,
      "callHierarchyProvider": true,
      "clangdInlayHintsProvider": true,
      "codeActionProvider": true,
      "compilationDatabase": {
        "automaticReload": true
      },
      "completionProvider": {
        "resolveProvider": false,
        "triggerCharacters": [
          ".",
          "<",
          ">",
          ":",
          "\"",
          "/",
          "*"
        ]
      },
      "declarationProvider": true,
      "definitionProvider": true,
      "documentFormattingProvider": true,
      "documentHighlightProvider": true,
      "documentLinkProvider": {
        "resolveProvider": false
      },
      "documentOnTypeFormattingProvider": {
        "firstTriggerCharacter": "\n",
        "moreTriggerCharacter": []
      },
      "documentRangeFormattingProvider": true,
      "documentSymbolProvider": true,
      "executeCommandProvider": {
        "commands": [
          "clangd.applyFix",
          "clangd.applyRename",
          "clangd.applyTweak"
        ]
      },
      "foldingRangeProvider": true,
      "hoverProvider": true,
      "implementationProvider": true,
      "inactiveRegionsProvider": true,
      "inlayHintProvider": true,
      "memoryUsageProvider": true,
      "referencesProvider": true,
      "renameProvider": true,
      "selectionRangeProvider": true,
      "semanticTokensProvider": {
        "full": {
          "delta": true
        },
        "legend": {
          "tokenModifiers": [
            "declaration",
            "definition",
            "deprecated",
            "deduced",
            "readonly",
            "static",
            "abstract",
            "virtual",
            "dependentName",
            "defaultLibrary",
            "usedAsMutableReference",
            "usedAsMutablePointer",
            "constructorOrDestructor",
            "userDefined",
            "functionScope",
            "classScope",
            "fileScope",
            "globalScope"
          ],
          "tokenTypes": [
            "variable",
            "variable",
            "parameter",
            "function",
            "method",
            "function",
            "property",
            "variable",
            "class",
            "interface",
            "enum",
            "enumMember",
            "type",
            "type",
            "unknown",
            "namespace",
            "typeParameter",
            "concept",
            "type",
            "macro",
            "modifier",
            "operator",
            "bracket",
            "label",
            "comment"
          ]
        },
        "range": false
      },
      "signatureHelpProvider": {
        "triggerCharacters": [
          "(",
          ")",
          "{",
          "}",
          "<",
          ">",
          ","
        ]
      },
      "standardTypeHierarchyProvider": true,
      "textDocumentSync": {
        "change": 2,
        "openClose": true,
        "save": true
      },
      "typeDefinitionProvider": true,
      "typeHierarchyProvider": true,
      "workspaceSymbolProvider": true
    },
    "serverInfo": {
      "name": "clangd",
      "version": "Debian clangd version 19.1.7 (3+b1) linux+grpc x86_64-pc-linux-gnu"
    }
  }
}
V[15:14:48.146] ASTWorker running Hover on version 1 of /home/taba1uga/Github/learning_clangd/cc/app/main.c
I[15:14:48.148] --> reply:textDocument/hover(5) 1 ms
V[15:14:48.148] >>> {
  "id": 5,
  "jsonrpc": "2.0",
  "result": {
    "contents": {
      "kind": "plaintext",
      "value": "function foo\nprovided by <lib.h>\n\n→ int\n\nint foo(void)"
    },
    "range": {
      "end": {
        "character": 5,
        "line": 3
      },
      "start": {
        "character": 2,
        "line": 3
      }
    }
  }
}

V[15:14:48.148] ASTWorker running Definitions on version 1 of /home/taba1uga/Github/learning_clangd/cc/app/main.c
I[15:14:48.148] --> reply:textDocument/definition(6) 1 ms
V[15:14:48.148] >>> {
  "id": 6,
  "jsonrpc": "2.0",
  "result": [
    {
      "range": {
        "end": {
          "character": 7,
          "line": 4
        },
        "start": {
          "character": 4,
          "line": 4
        }
      },
      "uri": "file:///home/taba1uga/Github/learning_clangd/cc/lib/lib.c"
    }
  ]
}

As the log indicates, finally we are being redirected to lib.c, sucesss!

Real world scenario

You may think now, your example very nice but why are you telling me about all this?

Because in real world, when you are gonna build your next app, you will propably need some libraries to use in the app and it's nice when your IDE know how to take you from you app code to any of your libraries code.

So let's consider another example, we want to build GTK app. We downloaded gtk source code into ~/gtk and glib code into ~/glib, and you want to develop your app once again in ~/app, so how can we tell clangd about all source files in ~/gtk and ~/glib?

GTK and GLIB both use meson, which can generate compile_commands.json for us, but we can't change by hand all paths in every compile command file we want to use, cause it would take ages and drive us mad, so what to do?

The trick here is to setup build directories of both libraries inside the app directory, this way all paths generated by meson will be relative to our app directory out of the box:

# For app
❯ cd app/
❯ mkdir build
❯ meson setup build/app
❯ meson compile -C build/app
...
❯ # For GLIB
❯ cd ../glib
❯ meson setup ../app/build/glib
❯ meson compile -C ../app/build/glib
...
# For GTK
❯ cd ../gtk
❯ meson setup ../app/build/gtk
❯ meson compile -C ../app/build/gtk
...

Now all we need to do is to take compile_commands.json from app/build/gtk and one from app/build/glib and add them to compile_commands.json in app/build/app, once it's done we are ready to run clangd once again:

❯ clangd-19 --pretty --log=verbose --compile-commands-dir=/home/taba1uga/app/build/app
I[11:57:42.077] Debian clangd version 19.1.7 (3+b1)
I[11:57:42.077] Features: linux+grpc
I[11:57:42.077] PID: 1399150
I[11:57:42.077] Working directory: /home/taba1uga/Github/learning_gtk4
I[11:57:42.077] argv[0]: clangd-19
I[11:57:42.077] argv[1]: --log=verbose
I[11:57:42.077] argv[2]: --pretty
I[11:57:42.077] argv[3]: --compile-commands-dir=/home/taba1uga/Github/app/build/app/
V[11:57:42.077] User config file is /home/taba1uga/.config/clangd/config.yaml
I[11:57:42.077] Starting LSP over stdin/stdout
V[11:57:42.078] <<< {
  "id": 1,
  "jsonrpc": "2.0",
  "method": "initialize",
  "params": {
    "capabilities": {
      "general": {
        "positionEncodings": [
          "utf-32",
          "utf-16"
        ]
      },
      "textDocument": {
        "callHierarchy": {
          "dynamicRegistration": false
        },
        "codeAction": {
          "codeActionLiteralSupport": {
            "codeActionKind": {
              "valueSet": [
                "",
                "quickfix",
                "refactor",
                "refactor.extract",
                "refactor.inline",
                "refactor.rewrite",
                "source",
                "source.organizeImports"
              ]
            }
          },
          "dataSupport": true,
          "dynamicRegistration": true,
          "isPreferredSupport": true,
          "resolveSupport": {
            "properties": [
              "edit",
              "command"
            ]
          }
        },
        "completion": {
          "completionItem": {
            "deprecatedSupport": true,
            "documentationFormat": [
              "markdown",
              "plaintext"
            ],
            "insertReplaceSupport": true,
            "insertTextModeSupport": {
              "valueSet": [
                1,
                2
              ]
            },
            "labelDetailsSupport": true,
            "resolveAdditionalTextEditsSupport": true,
            "resolveSupport": {
              "properties": [
                "documentation",
                "detail",
                "additionalTextEdits",
                "command"
              ]
            },
            "snippetSupport": true
          },
          "contextSupport": true,
          "dynamicRegistration": true
        },
        "declaration": {
          "dynamicRegistration": true,
          "linkSupport": true
        },
        "definition": {
          "dynamicRegistration": true,
          "linkSupport": true
        },
        "diagnostic": {
          "dynamicRegistration": false,
          "relatedDocumentSupport": false
        },
        "documentLink": {
          "dynamicRegistration": true,
          "tooltipSupport": true
        },
        "documentSymbol": {
          "hierarchicalDocumentSymbolSupport": true,
          "symbolKind": {
            "valueSet": [
              1,
              2,
              3,
              4,
              5,
              6,
              7,
              8,
              9,
              10,
              11,
              12,
              13,
              14,
              15,
              16,
              17,
              18,
              19,
              20,
              21,
              22,
              23,
              24,
              25,
              26
            ]
          }
        },
        "foldingRange": {
          "dynamicRegistration": true
        },
        "formatting": {
          "dynamicRegistration": true
        },
        "hover": {
          "contentFormat": [
            "markdown",
            "plaintext"
          ],
          "dynamicRegistration": true
        },
        "implementation": {
          "dynamicRegistration": true,
          "linkSupport": true
        },
        "inlineCompletion": null,
        "linkedEditingRange": {
          "dynamicRegistration": true
        },
        "onTypeFormatting": {
          "dynamicRegistration": true
        },
        "publishDiagnostics": {
          "relatedInformation": true,
          "tagSupport": {
            "valueSet": [
              1,
              2
            ]
          },
          "versionSupport": true
        },
        "rangeFormatting": {
          "dynamicRegistration": true
        },
        "references": {
          "dynamicRegistration": true
        },
        "rename": {
          "dynamicRegistration": true,
          "prepareSupport": true
        },
        "selectionRange": {
          "dynamicRegistration": true
        },
        "signatureHelp": {
          "dynamicRegistration": true,
          "signatureInformation": {
            "activeParameterSupport": true,
            "parameterInformation": {
              "labelOffsetSupport": true
            }
          }
        },
        "synchronization": {
          "didSave": true,
          "willSave": true,
          "willSaveWaitUntil": true
        },
        "typeDefinition": {
          "dynamicRegistration": true,
          "linkSupport": true
        },
        "typeHierarchy": {
          "dynamicRegistration": true
        }
      },
      "window": {
        "showDocument": {
          "support": true
        },
        "workDoneProgress": true
      },
      "workspace": {
        "applyEdit": true,
        "codeLens": {
          "refreshSupport": true
        },
        "configuration": true,
        "diagnostics": {
          "refreshSupport": false
        },
        "didChangeWatchedFiles": {
          "dynamicRegistration": true
        },
        "executeCommand": {
          "dynamicRegistration": false
        },
        "fileOperations": {
          "didCreate": false,
          "didDelete": false,
          "didRename": true,
          "willCreate": false,
          "willDelete": false,
          "willRename": true
        },
        "symbol": {
          "symbolKind": {
            "valueSet": [
              1,
              2,
              3,
              4,
              5,
              6,
              7,
              8,
              9,
              10,
              11,
              12,
              13,
              14,
              15,
              16,
              17,
              18,
              19,
              20,
              21,
              22,
              23,
              24,
              25,
              26
            ]
          }
        },
        "workspaceEdit": {
          "documentChanges": true,
          "resourceOperations": [
            "create",
            "rename",
            "delete"
          ]
        },
        "workspaceFolders": true
      }
    },
    "clientInfo": {
      "name": "emacs",
      "version": "GNU Emacs 30.1 (build 1, x86_64-pc-linux-gnu, GTK+ Version 3.24.49, cairo version 1.18.4)\n of 2025-07-18, modified by Debian"
    },
    "initializationOptions": null,
    "processId": 1398817,
    "rootPath": "/home/taba1uga/Github/learning_gtk4",
    "rootUri": "file:///home/taba1uga/Github/learning_gtk4",
    "workDoneToken": "1"
  }
}

I[11:57:42.078] <-- initialize(1)
I[11:57:42.079] --> reply:initialize(1) 1 ms
I[12:00:31.646] <-- textDocument/codeAction(77)
V[12:00:31.647] ASTWorker running codeAction on version 0 of /home/taba1uga/Github/app/main.c
I[12:00:31.647] --> reply:textDocument/codeAction(77) 0 ms
V[12:00:31.647] >>> {
  "id": 77,
  "jsonrpc": "2.0",
  "result": []
}

V[12:00:32.696] <<< {
  "id": 78,
  "jsonrpc": "2.0",
  "method": "textDocument/definition",
  "params": {
    "position": {
      "character": 12,
      "line": 10
    },
    "textDocument": {
      "uri": "file:///home/taba1uga/Github/app/main.c"
    }
  }
}

I[12:00:32.696] <-- textDocument/definition(78)
V[12:00:32.696] ASTWorker running Definitions on version 0 of /home/taba1uga/Github/app/main.c
I[12:00:32.696] --> reply:textDocument/definition(78) 0 ms
V[12:00:32.696] >>> {
  "id": 78,
  "jsonrpc": "2.0",
  "result": [
    {
      "range": {
        "end": {
          "character": 17,
          "line": 2616
        },
        "start": {
          "character": 0,
          "line": 2616
        }
      },
      "uri": "file:///home/taba1uga/glib/gio/gapplication.c"
    }
  ]
}

Success once again, oh clangd you usefull fellow.

Conclusions

The key takeaway is simple: clangd can only navigate what it knows about. If a library's source files are missing from the compilation database, clangd will fall back to headers, limiting features like "go to definition". Once those sources are included, clangd allow you to traverse application and library code as if they were a single project. All thanks to compile_commands.json.

Published:

Last updated:

Category: C

Comments

Leave a comment