Skip to content

Commit

Permalink
Merge pull request #8699 from tweag/nix-c-bindings
Browse files Browse the repository at this point in the history
(Towards) stable C bindings for libutil, libexpr
  • Loading branch information
roberth authored Apr 4, 2024
2 parents 50cb14f + 926fbad commit 12ec315
Show file tree
Hide file tree
Showing 47 changed files with 3,624 additions and 20 deletions.
19 changes: 18 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,20 @@ makefiles = \
src/libexpr/local.mk \
src/libcmd/local.mk \
src/nix/local.mk \
src/libutil-c/local.mk \
src/libstore-c/local.mk \
src/libexpr-c/local.mk \
src/resolve-system-dependencies/local.mk \
scripts/local.mk \
misc/bash/local.mk \
misc/fish/local.mk \
misc/zsh/local.mk \
misc/systemd/local.mk \
misc/launchd/local.mk \
misc/upstart/local.mk
misc/upstart/local.mk \
doc/manual/local.mk \
doc/internal-api/local.mk \
doc/external-api/local.mk
endif

ifeq ($(ENABLE_UNIT_TESTS), yes)
Expand Down Expand Up @@ -60,6 +66,10 @@ ifeq ($(ENABLE_INTERNAL_API_DOCS), yes)
makefiles-late += doc/internal-api/local.mk
endif

ifeq ($(ENABLE_EXTERNAL_API_DOCS), yes)
makefiles-late += doc/external-api/local.mk
endif

# Miscellaneous global Flags

OPTIMIZE = 1
Expand Down Expand Up @@ -124,3 +134,10 @@ internal-api-html:
@echo "Internal API docs are disabled. Configure with '--enable-internal-api-docs', or avoid calling 'make internal-api-html'."
@exit 1
endif

ifneq ($(ENABLE_EXTERNAL_API_DOCS), yes)
.PHONY: external-api-html
external-api-html:
@echo "External API docs are disabled. Configure with '--enable-external-api-docs', or avoid calling 'make external-api-html'."
@exit 1
endif
1 change: 1 addition & 0 deletions Makefile.config.in
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ ENABLE_BUILD = @ENABLE_BUILD@
ENABLE_DOC_GEN = @ENABLE_DOC_GEN@
ENABLE_FUNCTIONAL_TESTS = @ENABLE_FUNCTIONAL_TESTS@
ENABLE_INTERNAL_API_DOCS = @ENABLE_INTERNAL_API_DOCS@
ENABLE_EXTERNAL_API_DOCS = @ENABLE_EXTERNAL_API_DOCS@
ENABLE_S3 = @ENABLE_S3@
ENABLE_UNIT_TESTS = @ENABLE_UNIT_TESTS@
GTEST_LIBS = @GTEST_LIBS@
Expand Down
9 changes: 9 additions & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@ AC_ARG_ENABLE(unit-tests, AS_HELP_STRING([--disable-unit-tests],[Do not build th
ENABLE_UNIT_TESTS=$enableval, ENABLE_UNIT_TESTS=$ENABLE_BUILD)
AC_SUBST(ENABLE_UNIT_TESTS)

# Build external API docs by default
AC_ARG_ENABLE(external_api_docs, AS_HELP_STRING([--enable-external-api-docs],[Build API docs for Nix's C interface]),
external_api_docs=$enableval, external_api_docs=yes)
AC_SUBST(external_api_docs)

AS_IF(
[test "$ENABLE_BUILD" == "no" && test "$ENABLE_UNIT_TESTS" == "yes"],
[AC_MSG_ERROR([Cannot enable unit tests when building overall is disabled. Please do not pass '--enable-unit-tests' or do not pass '--disable-build'.])])
Expand All @@ -172,6 +177,10 @@ AC_ARG_ENABLE(internal-api-docs, AS_HELP_STRING([--enable-internal-api-docs],[Bu
ENABLE_INTERNAL_API_DOCS=$enableval, ENABLE_INTERNAL_API_DOCS=no)
AC_SUBST(ENABLE_INTERNAL_API_DOCS)

AC_ARG_ENABLE(external-api-docs, AS_HELP_STRING([--enable-external-api-docs],[Build API docs for Nix's external unstable C interfaces]),
ENABLE_EXTERNAL_API_DOCS=$enableval, ENABLE_EXTERNAL_API_DOCS=no)
AC_SUBST(ENABLE_EXTERNAL_API_DOCS)

AS_IF(
[test "$ENABLE_FUNCTIONAL_TESTS" == "yes" || test "$ENABLE_DOC_GEN" == "yes"],
[NEED_PROG(jq, jq)])
Expand Down
3 changes: 3 additions & 0 deletions doc/external-api/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/doxygen.cfg
/html
/latex
108 changes: 108 additions & 0 deletions doc/external-api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Getting started

> **Warning** These bindings are **experimental**, which means they can change
> at any time or be removed outright; nevertheless the plan is to provide a
> stable external C API to the Nix language and the Nix store.
The language library allows evaluating Nix expressions and interacting with Nix
language values. The Nix store API is still rudimentary, and only allows
initialising and connecting to a store for the Nix language evaluator to
interact with.

Currently there are two ways to interface with the Nix language evaluator
programmatically:

1. Embedding the evaluator
2. Writing language plug-ins

Embedding means you link the Nix C libraries in your program and use them from
there. Adding a plug-in means you make a library that gets loaded by the Nix
language evaluator, specified through a configuration option.

Many of the components and mechanisms involved are not yet documented, therefore
please refer to the [Nix source code](https://github.com/NixOS/nix/) for
details. Additions to in-code documentation and the reference manual are highly
appreciated.

The following examples, for simplicity, don't include error handling. See the
[Handling errors](@ref errors) section for more information.

# Embedding the Nix Evaluator

In this example we programmatically start the Nix language evaluator with a
dummy store (that has no store paths and cannot be written to), and evaluate the
Nix expression `builtins.nixVersion`.

**main.c:**

```C
#include <nix_api_util.h>
#include <nix_api_expr.h>
#include <nix_api_value.h>
#include <stdio.h>

// NOTE: This example lacks all error handling. Production code must check for
// errors, as some return values will be undefined.
int main() {
nix_libexpr_init(NULL);

Store* store = nix_store_open(NULL, "dummy://", NULL);
EvalState* state = nix_state_create(NULL, NULL, store); // empty search path (NIX_PATH)
Value *value = nix_alloc_value(NULL, state);

nix_expr_eval_from_string(NULL, state, "builtins.nixVersion", ".", value);
nix_value_force(NULL, state, value);
printf("Nix version: %s\n", nix_get_string(NULL, value));

nix_gc_decref(NULL, value);
nix_state_free(state);
nix_store_free(store);
return 0;
}
```

**Usage:**

```ShellSession
$ gcc main.c $(pkg-config nix-expr-c --libs --cflags) -o main
$ ./main
Nix version: 2.17
```

# Writing a Nix language plug-in

In this example we add a custom primitive operation (_primop_) to `builtins`. It
will increment the argument if it is an integer and throw an error otherwise.

**plugin.c:**

```C
#include <nix_api_util.h>
#include <nix_api_expr.h>
#include <nix_api_value.h>

void increment(void* user_data, nix_c_context* ctx, EvalState* state, Value** args, Value* v) {
nix_value_force(NULL, state, args[0]);
if (nix_get_type(NULL, args[0]) == NIX_TYPE_INT) {
nix_init_int(NULL, v, nix_get_int(NULL, args[0]) + 1);
} else {
nix_set_err_msg(ctx, NIX_ERR_UNKNOWN, "First argument should be an integer.");
}
}

void nix_plugin_entry() {
const char* args[] = {"n", NULL};
PrimOp *p = nix_alloc_primop(NULL, increment, 1, "increment", args, "Example custom built-in function: increments an integer", NULL);
nix_register_primop(NULL, p);
nix_gc_decref(NULL, p);
}
```
**Usage:**
```ShellSession
$ gcc plugin.c $(pkg-config nix-expr-c --libs --cflags) -shared -o plugin.so
$ nix --plugin-files ./plugin.so repl
nix-repl> builtins.increment 1
2
```
57 changes: 57 additions & 0 deletions doc/external-api/doxygen.cfg.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Doxyfile 1.9.5

# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by
# double-quotes, unless you are using Doxywizard) that should identify the
# project for which the documentation is generated. This name is used in the
# title of most generated pages and in a few other places.
# The default value is: My Project.

PROJECT_NAME = "Nix"

# The PROJECT_NUMBER tag can be used to enter a project or revision number. This
# could be handy for archiving the generated documentation or if some version
# control system is used.

PROJECT_NUMBER = @PACKAGE_VERSION@

# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a
# quick idea about the purpose of the project. Keep the description short.

PROJECT_BRIEF = "Nix, the purely functional package manager: C API (experimental)"

# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output.
# The default value is: YES.

GENERATE_LATEX = NO

# The INPUT tag is used to specify the files and/or directories that contain
# documented source files. You may enter file names like myfile.cpp or
# directories like /usr/src/myproject. Separate the files or directories with
# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
# Note: If this tag is empty the current directory is searched.

# FIXME Make this list more maintainable somehow. We could maybe generate this
# in the Makefile, but we would need to change how `.in` files are preprocessed
# so they can expand variables despite configure variables.

INPUT = \
src/libutil-c \
src/libexpr-c \
src/libstore-c \
doc/external-api/README.md

FILE_PATTERNS = nix_api_*.h *.md

# The INCLUDE_PATH tag can be used to specify one or more directories that
# contain include files that are not input files but should be processed by the
# preprocessor. Note that the INCLUDE_PATH is not recursive, so the setting of
# RECURSIVE has no effect here.
# This tag requires that the tag SEARCH_INCLUDES is set to YES.

INCLUDE_PATH = @RAPIDCHECK_HEADERS@
EXCLUDE_PATTERNS = *_internal.h
GENERATE_TREEVIEW = YES
OPTIMIZE_OUTPUT_FOR_C = YES

USE_MDFILE_AS_MAINPAGE = doc/external-api/README.md
7 changes: 7 additions & 0 deletions doc/external-api/local.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
$(docdir)/external-api/html/index.html $(docdir)/external-api/latex: $(d)/doxygen.cfg
mkdir -p $(docdir)/external-api
{ cat $< ; echo "OUTPUT_DIRECTORY=$(docdir)/external-api" ; } | doxygen -

# Generate the HTML API docs for Nix's unstable C bindings
.PHONY: external-api-html
external-api-html: $(docdir)/external-api/html/index.html
7 changes: 7 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,13 @@
enableInternalAPIDocs = true;
};

# API docs for Nix's C bindings.
external-api-docs = nixpkgsFor.x86_64-linux.native.callPackage ./package.nix {
inherit fileset;
doBuild = false;
enableExternalAPIDocs = true;
};

# System tests.
tests = import ./tests/nixos { inherit lib nixpkgs nixpkgsFor; } // {

Expand Down
2 changes: 1 addition & 1 deletion local.mk
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ GLOBAL_CXXFLAGS += -Wno-deprecated-declarations -Werror=switch
# Allow switch-enum to be overridden for files that do not support it, usually because of dependency headers.
ERROR_SWITCH_ENUM = -Werror=switch-enum

$(foreach i, config.h $(wildcard src/lib*/*.hh), \
$(foreach i, config.h $(wildcard src/lib*/*.hh) $(wildcard src/lib*/*.h $(filter-out %_internal.h, $(wildcard src/lib*c/*.h))), \
$(eval $(call install-file-in, $(i), $(includedir)/nix, 0644)))

ifdef HOST_UNIX
Expand Down
19 changes: 15 additions & 4 deletions package.nix
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
, autoreconfHook
, aws-sdk-cpp
, boehmgc
, buildPackages
, nlohmann_json
, bison
, boost
Expand Down Expand Up @@ -91,9 +92,10 @@
# - readline
, readlineFlavor ? if stdenv.hostPlatform.isWindows then "readline" else "editline"

# Whether to build the internal API docs, can be done separately from
# Whether to build the internal/external API docs, can be done separately from
# everything else.
, enableInternalAPIDocs ? false
, enableExternalAPIDocs ? false

# Whether to install unit tests. This is useful when cross compiling
# since we cannot run them natively during the build, but can do so
Expand Down Expand Up @@ -182,6 +184,9 @@ in {
./doc/manual
] ++ lib.optionals enableInternalAPIDocs [
./doc/internal-api
] ++ lib.optionals enableExternalAPIDocs [
./doc/external-api
] ++ lib.optionals (enableInternalAPIDocs || enableExternalAPIDocs) [
# Source might not be compiled, but still must be available
# for Doxygen to gather comments.
./src
Expand All @@ -199,7 +204,7 @@ in {
++ lib.optional doBuild "dev"
# If we are doing just build or just docs, the one thing will use
# "out". We only need additional outputs if we are doing both.
++ lib.optional (doBuild && (enableManual || enableInternalAPIDocs)) "doc"
++ lib.optional (doBuild && (enableManual || enableInternalAPIDocs || enableExternalAPIDocs)) "doc"
++ lib.optional installUnitTests "check";

nativeBuildInputs = [
Expand All @@ -221,7 +226,7 @@ in {
] ++ lib.optionals (doInstallCheck || enableManual) [
jq # Also for custom mdBook preprocessor.
] ++ lib.optional stdenv.hostPlatform.isLinux util-linux
++ lib.optional enableInternalAPIDocs doxygen
++ lib.optional (enableInternalAPIDocs || enableExternalAPIDocs) doxygen
;

buildInputs = lib.optionals doBuild [
Expand Down Expand Up @@ -285,6 +290,7 @@ in {
(lib.enableFeature buildUnitTests "unit-tests")
(lib.enableFeature doInstallCheck "functional-tests")
(lib.enableFeature enableInternalAPIDocs "internal-api-docs")
(lib.enableFeature enableExternalAPIDocs "external-api-docs")
(lib.enableFeature enableManual "doc-gen")
(lib.enableFeature enableGC "gc")
(lib.enableFeature enableMarkdown "markdown")
Expand All @@ -309,7 +315,8 @@ in {
makeFlags = "profiledir=$(out)/etc/profile.d PRECOMPILE_HEADERS=1";

installTargets = lib.optional doBuild "install"
++ lib.optional enableInternalAPIDocs "internal-api-html";
++ lib.optional enableInternalAPIDocs "internal-api-html"
++ lib.optional enableExternalAPIDocs "external-api-html";

installFlags = "sysconfdir=$(out)/etc";

Expand All @@ -336,6 +343,10 @@ in {
'' + lib.optionalString enableInternalAPIDocs ''
mkdir -p ''${!outputDoc}/nix-support
echo "doc internal-api-docs $out/share/doc/nix/internal-api/html" >> ''${!outputDoc}/nix-support/hydra-build-products
''
+ lib.optionalString enableExternalAPIDocs ''
mkdir -p ''${!outputDoc}/nix-support
echo "doc external-api-docs $out/share/doc/nix/external-api/html" >> ''${!outputDoc}/nix-support/hydra-build-products
'';

# So the check output gets links for DLLs in the out output.
Expand Down
25 changes: 25 additions & 0 deletions src/libexpr-c/local.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
libraries += libexprc

libexprc_NAME = libnixexprc

libexprc_DIR := $(d)

libexprc_SOURCES := \
$(wildcard $(d)/*.cc) \

# Not just for this library itself, but also for downstream libraries using this library

INCLUDE_libexprc := -I $(d)
libexprc_CXXFLAGS += $(INCLUDE_libutil) $(INCLUDE_libutilc) \
$(INCLUDE_libfetchers) \
$(INCLUDE_libstore) $(INCLUDE_libstorec) \
$(INCLUDE_libexpr) $(INCLUDE_libexprc)

libexprc_LIBS = libutil libutilc libstore libstorec libexpr

libexprc_LDFLAGS += -pthread

$(eval $(call install-file-in, $(d)/nix-expr-c.pc, $(libdir)/pkgconfig, 0644))

libexprc_FORCE_INSTALL := 1

10 changes: 10 additions & 0 deletions src/libexpr-c/nix-expr-c.pc.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
prefix=@prefix@
libdir=@libdir@
includedir=@includedir@

Name: Nix
Description: Nix Language Evaluator - C API
Version: @PACKAGE_VERSION@
Requires: nix-store-c
Libs: -L${libdir} -lnixexprc
Cflags: -I${includedir}/nix
Loading

0 comments on commit 12ec315

Please sign in to comment.