| name | DuckDB-Extensions |
| description | Building DuckDB loadable extensions with CMake, FetchContent, and proper metadata handling |
DuckDB Extensions
This skill covers building DuckDB loadable extensions (.duckdb_extension files) using CMake and FetchContent. Extensions must include proper metadata to be loaded by DuckDB.
Instructions
When helping users build DuckDB extensions, follow these guidelines:
-
Version Matching: The extension MUST be built against the exact same DuckDB version that will load it. Use FetchContent with a specific git tag (e.g., v1.2.1).
-
Metadata is Required: DuckDB extensions need metadata appended via the append_metadata.cmake script. Without this, DuckDB will reject the extension with "not a DuckDB extension" error.
-
Static Linking: For portable extensions, link against duckdb_static with visibility hidden and exclude-libs flags.
-
Table Function State: In DuckDB 1.1+, bind_data is const during execution. Use GlobalTableFunctionState for mutable state accessed via input.global_state.
-
PIC Required: Static libraries linked into the shared extension must be built with POSITION_INDEPENDENT_CODE ON.
Examples
Example 1: Platform Mismatch Error
User: I get "built for platform 'linux_amd64', but we can only load extensions built for platform 'linux_amd64_gcc4'"
Claude: This means your system DuckDB was built with the gcc4 ABI. Options:
1. Use the DuckDB binary from your extension build (in `_deps/duckdb-build/duckdb`)
2. Or match your build platform to the system DuckDB
The built-from-source DuckDB will be at:
`build/release/_deps/duckdb-build/duckdb`
Example 2: "Not a DuckDB extension" Error
User: DuckDB says "The file is not a DuckDB extension. The metadata at the end of the file is invalid"
Claude: The extension is missing required metadata. Add this POST_BUILD command:
add_custom_command(
TARGET ${TARGET_NAME}
POST_BUILD
COMMAND ${CMAKE_COMMAND}
-DABI_TYPE=CPP
-DEXTENSION=$<TARGET_FILE:${TARGET_NAME}>
-DPLATFORM_FILE=${duckdb_BINARY_DIR}/duckdb_platform_out
-DVERSION_FIELD=v1.2.1
-DEXTENSION_VERSION=v1.2.1
-DNULL_FILE=${duckdb_SOURCE_DIR}/scripts/null.txt
-P ${duckdb_SOURCE_DIR}/scripts/append_metadata.cmake
)
add_dependencies(${TARGET_NAME} duckdb_platform)
Example 3: Const bind_data Error
User: I get "increment of member in read-only object" when trying to modify bind_data
Claude: In newer DuckDB, bind_data is const during execution. Move mutable state to GlobalTableFunctionState:
struct MyBindData : public TableFunctionData {
// Immutable data set during bind
std::vector<std::string> items;
};
struct MyState : public GlobalTableFunctionState {
// Mutable state for execution
idx_t offset = 0;
};
Then register an init_global function:
func.init_global = [](ClientContext &ctx, TableFunctionInitInput &input) {
return make_uniq<MyState>();
};
Access both in execute:
auto &bind = input.bind_data->Cast<MyBindData>();
auto &state = input.global_state->Cast<MyState>();
Reference Implementation Details
CMakeLists.txt Template
Purpose: Complete CMake setup for a DuckDB extension with FetchContent
cmake_minimum_required(VERSION 3.21)
set(TARGET_NAME myextension)
set(EXTENSION_NAME ${TARGET_NAME}_extension)
set(LOADABLE_EXTENSION_NAME ${TARGET_NAME}_loadable_extension)
project(${TARGET_NAME})
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(FetchContent)
# Extension sources
set(EXTENSION_SOURCES
src/myextension.cpp
src/table_functions.cpp
)
# Fetch DuckDB - use exact version tag
FetchContent_Declare(
duckdb
GIT_REPOSITORY https://github.com/duckdb/duckdb.git
GIT_TAG v1.2.1
)
FetchContent_MakeAvailable(duckdb)
include_directories(${duckdb_SOURCE_DIR}/src/include)
# Build loadable extension
add_library(${LOADABLE_EXTENSION_NAME} SHARED ${EXTENSION_SOURCES})
# Required for static linking
set_target_properties(${LOADABLE_EXTENSION_NAME} PROPERTIES CXX_VISIBILITY_PRESET hidden)
target_link_libraries(${LOADABLE_EXTENSION_NAME}
PRIVATE
duckdb_static
-Wl,--gc-sections
-Wl,--exclude-libs,ALL
)
target_include_directories(${LOADABLE_EXTENSION_NAME}
PRIVATE
${CMAKE_SOURCE_DIR}/src/include
)
target_compile_definitions(${LOADABLE_EXTENSION_NAME} PUBLIC -DDUCKDB_BUILD_LOADABLE_EXTENSION)
set_target_properties(${LOADABLE_EXTENSION_NAME} PROPERTIES
OUTPUT_NAME ${TARGET_NAME}
PREFIX ""
SUFFIX ".duckdb_extension"
)
# Version must match FetchContent tag
set(DUCKDB_VERSION_NORMALIZED "v1.2.1")
set(NULL_FILE ${duckdb_SOURCE_DIR}/scripts/null.txt)
# Add metadata (REQUIRED for DuckDB to load the extension)
add_custom_command(
TARGET ${LOADABLE_EXTENSION_NAME}
POST_BUILD
COMMAND ${CMAKE_COMMAND}
-DABI_TYPE=CPP
-DEXTENSION=$<TARGET_FILE:${LOADABLE_EXTENSION_NAME}>
-DPLATFORM_FILE=${duckdb_BINARY_DIR}/duckdb_platform_out
-DVERSION_FIELD=${DUCKDB_VERSION_NORMALIZED}
-DEXTENSION_VERSION=${DUCKDB_VERSION_NORMALIZED}
-DNULL_FILE=${NULL_FILE}
-P ${duckdb_SOURCE_DIR}/scripts/append_metadata.cmake
)
add_dependencies(${LOADABLE_EXTENSION_NAME} duckdb_platform)
Key Points:
FetchContent_MakeAvailable(duckdb) provides duckdb_SOURCE_DIR and duckdb_BINARY_DIR
- The
duckdb_platform target generates duckdb_platform_out file needed for metadata
- Version string MUST include the
v prefix (e.g., v1.2.1 not 1.2.1)
Table Function Pattern (DuckDB 1.1+)
Purpose: Correct pattern for table functions with mutable execution state
#include "duckdb.hpp"
#include "duckdb/function/table_function.hpp"
#include "duckdb/main/extension_util.hpp"
namespace duckdb {
struct MyTableBindData : public TableFunctionData {
std::vector<std::string> items;
};
struct MyTableState : public GlobalTableFunctionState {
idx_t offset = 0;
};
static unique_ptr<FunctionData> MyTableBind(
ClientContext &context,
TableFunctionBindInput &input,
vector<LogicalType> &return_types,
vector<string> &names) {
return_types.push_back(LogicalType::VARCHAR);
names.push_back("item");
auto result = make_uniq<MyTableBindData>();
result->items = {"one", "two", "three"};
return std::move(result);
}
static unique_ptr<GlobalTableFunctionState> MyTableInitGlobal(
ClientContext &context, TableFunctionInitInput &input) {
return make_uniq<MyTableState>();
}
static void MyTableExecute(
ClientContext &context,
TableFunctionInput &input,
DataChunk &output) {
auto &bind_data = input.bind_data-><MyTableBindData>();
&state = input.global_state-><MyTableState>();
count = ;
(state.offset < bind_data.items.() && count < STANDARD_VECTOR_SIZE) {
output.(, count, (bind_data.items[state.offset]));
state.offset++;
count++;
}
output.(count);
}
{
;
func.init_global = MyTableInitGlobal;
ExtensionUtil::(db, func);
}
}
Extension Entry Point
Purpose: Required entry point functions for DuckDB to initialize the extension
#define DUCKDB_EXTENSION_MAIN
#include "duckdb.hpp"
#include "duckdb/main/extension_util.hpp"
namespace duckdb {
void RegisterMyTableFunction(DatabaseInstance &db);
static void LoadInternal(DatabaseInstance &instance) {
RegisterMyTableFunction(instance);
}
void MyextensionExtensionLoad(DuckDB &db) {
LoadInternal(*db.instance);
}
std::string MyextensionExtensionVersion() {
return "v1.0.0";
}
}
extern "C" {
DUCKDB_EXTENSION_API void myextension_init(duckdb::DatabaseInstance &db) {
duckdb::LoadInternal(db);
}
DUCKDB_EXTENSION_API const char *myextension_version() {
return duckdb::MyextensionExtensionVersion().c_str();
}
}
Key Points:
- Function names in
extern "C" block must match extension name: {name}_init and {name}_version
DUCKDB_EXTENSION_API macro ensures proper symbol visibility
Loading Extensions
LOAD '/path/to/myextension.duckdb_extension';
Troubleshooting
Version Mismatch Error
Error: "built specifically for DuckDB version 'X' and can only be loaded with that version"
Cause: Extension metadata version doesn't match running DuckDB
Solution: Ensure DUCKDB_VERSION_NORMALIZED in CMake matches the DuckDB tag AND includes the v prefix
PIC/Relocation Error
Error: "relocation R_X86_64_PC32 against symbol... recompile with -fPIC"
Cause: Static library being linked into shared library without PIC
Solution: Add to any static library targets:
set_target_properties(mylib PROPERTIES POSITION_INDEPENDENT_CODE ON)
Missing duckdb_platform Dependency
Error: "duckdb_platform_out" file not found during metadata append
Solution: Ensure add_dependencies(${TARGET_NAME} duckdb_platform) is present
Deployment & Configuration
Extension Installation Path
DuckDB looks for extensions in:
~/.duckdb/extensions/v{version}/{platform}/
For example:
~/.duckdb/extensions/v1.2.1/linux_amd64/myextension.duckdb_extension
Copy your built extension there for automatic discovery:
cp build/release/myextension.duckdb_extension ~/.duckdb/extensions/v1.2.1/linux_amd64/
Auto-Loading with ~/.duckdbrc
Create ~/.duckdbrc to automatically load extensions and configure settings on startup:
LOAD 'myextension';
SET myextension_host = 'server.example.com';
SET myextension_port = 50051;
Important: Settings registered by an extension are only available AFTER the extension loads. Put LOAD first, then SET.
Wrapper Script for Unsigned Extensions
During development, unsigned extensions require the -unsigned flag. Create a wrapper script to avoid forgetting:
#!/bin/bash
exec /path/to/actual/duckdb -unsigned "$@"
Make it executable and ensure ~/.local/bin is in your PATH before the actual duckdb location.
Registering Custom Settings
Extensions can register custom settings that users can configure:
#include "duckdb/main/config.hpp"
static void LoadInternal(DatabaseInstance &instance) {
auto &config = DBConfig::GetConfig(instance);
config.AddExtensionOption("myextension_host", "Server hostname",
LogicalType::VARCHAR, Value("localhost"));
config.AddExtensionOption("myextension_port", "Server port",
LogicalType::INTEGER, Value(50051));
}
Access settings in your code:
Value host_value, port_value;
if (context.TryGetCurrentSetting("myextension_host", host_value)) {
std::string host = host_value.GetValue<std::string>();
}
Best Practices Summary
- Always use exact version tags with FetchContent, never
main or master
- Version string must include
v prefix to match DuckDB's format
- Test with the DuckDB binary from the same build before system DuckDB
- Use
GlobalTableFunctionState for any mutable state during table function execution
- Set
POSITION_INDEPENDENT_CODE ON for all static libraries linked into the extension
- Install extensions to
~/.duckdb/extensions/v{version}/{platform}/ for auto-discovery
- In
.duckdbrc, always LOAD before SET for extension settings