Skip to content

C API: add logger bindings#15816

Open
RossComputerGuy wants to merge 1 commit into
NixOS:masterfrom
DeterminateSystems:upstream-nix-capi-logger
Open

C API: add logger bindings#15816
RossComputerGuy wants to merge 1 commit into
NixOS:masterfrom
DeterminateSystems:upstream-nix-capi-logger

Conversation

@RossComputerGuy
Copy link
Copy Markdown
Member

Motivation

Closes #11036, upstreaming of DeterminateSystems#448

Context

Having Nix directly use stderr/stdout can cause problems with certain logging libraries in Rust or C. Being able to override the logger so the program integrating Nix via the C API allows for unified logging. I've tested this with Rust and it works great with tracing.


Add 👍 to pull requests you find important.

The Nix maintainer team uses a GitHub project board to schedule and track reviews.

@RossComputerGuy RossComputerGuy requested a review from edolstra as a code owner May 8, 2026 20:48
@github-actions github-actions Bot added the c api Nix as a C library with a stable interface label May 8, 2026
Comment thread src/libutil-c/nix_api_util.cc Outdated
Comment thread src/libutil-c/nix_api_util.cc Outdated
Comment thread src/libutil-c/nix_api_util.cc Outdated
@RossComputerGuy RossComputerGuy force-pushed the upstream-nix-capi-logger branch from 06e5956 to d5d7b4b Compare May 11, 2026 23:10
Comment thread src/libutil-tests/nix_api_util.cc Outdated
Copy link
Copy Markdown
Contributor

@xokdvium xokdvium left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we not want to also expose progress in the C API?

Comment thread src/libutil-c/nix_api_util.cc Outdated
@RossComputerGuy RossComputerGuy force-pushed the upstream-nix-capi-logger branch from d5d7b4b to b398d2c Compare May 11, 2026 23:25
@RossComputerGuy
Copy link
Copy Markdown
Member Author

Do we not want to also expose progress in the C API?

Isn't that what result does?

@xokdvium
Copy link
Copy Markdown
Contributor

Isn't that what result does?

Ah right yeah, it's a thin wrapper around result. Though we'd want to expose integer fields over the bindings as well for this to be fully usable.

@xokdvium
Copy link
Copy Markdown
Contributor

Tbh I love this idea and the possibility of writing a C plugin that implements something like nom as a plugin. Together with #15696 this would be amazing.

@RossComputerGuy
Copy link
Copy Markdown
Member Author

Isn't that what result does?

Ah right yeah, it's a thin wrapper around result. Though we'd want to expose integer fields over the bindings as well for this to be fully usable.

Yes, working on that.

Tbh I love this idea and the possibility of writing a C plugin that implements something like nom as a plugin. Together with #15696 this would be amazing.

That would be cool. Having a more Nix native nom would be nice, especially on systems where Haskell isn't supported.

@RossComputerGuy RossComputerGuy force-pushed the upstream-nix-capi-logger branch from b398d2c to fd7ee66 Compare May 12, 2026 00:02
Comment thread src/libutil-c/nix_api_util.cc Outdated
nix::Verbosity lvl,
nix::ActivityType type,
const std::string & s,
const Fields & /*fields*/,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably also expose the fields to the callback, since it contains valueable info about which URL is being downloaded e.t.c.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread src/libutil-c/nix_api_util.cc Outdated
Comment on lines +85 to +86
std::vector<nix_logger_field> storage;
storage.reserve(fields.size());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we maybe allocate this as a small_vector? Typically there are only a handful of fields (<4), so we could avoid the allocation overhead there completely?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread src/libutil-c/nix_api_util.h Outdated
Comment on lines +454 to +456
uint64_t i;
const char * str;
unsigned int len;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be an anonymous union with anonymous struct members? Pretty sure it wouldn't pose much of a risk to ABI backwards compatibility if we added a type of field for example (as long we only extend the union). We should also document that the size of this type could grow in a backwards compatible way.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about that but remembered some languages with C FFI, like Zig and Dart, don't quite like anonymous struct members.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm maybe we can avoid the GNU extension and just do regular unions? Though I guess it doesn't matter much if it poses a compat risk

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, yeah we could do a regular union we typedef. Maybe call it nix_logger_field_value.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread src/libutil-c/nix_api_util.cc Outdated
Comment on lines +82 to +83
if (fields.empty())
return;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure that we don't want to signal the callback even when there are no fields?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure, I'll remove the check and we'll see.

Comment thread src/libutil-c/nix_api_util.cc Outdated
storage.push_back(f);
}

std::vector<const nix_logger_field *> cfields;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small vector would work nicely here too.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see small vector in the STL docs.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We polyfill with boost's small_vector... in libexpr and other places at least

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha, done

Comment thread src/libutil-c/nix_api_util.cc Outdated
Comment thread src/libutil-tests/nix_api_util.cc Outdated
Comment thread src/libutil-tests/nix_api_util.cc
@RossComputerGuy RossComputerGuy force-pushed the upstream-nix-capi-logger branch 4 times, most recently from f3bc162 to 93e3022 Compare May 13, 2026 22:44
Copy link
Copy Markdown
Contributor

@xokdvium xokdvium left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess one open question is how / whether a logger can report errors? Should it be able to or should it just swallow all errors that it might encounter internally? I guess it would be good for robustness.

Copy link
Copy Markdown
Contributor

@xokdvium xokdvium left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking great. Now just the tiny question of error handling and how we want to address that (or whether we want to at all)

Comment thread src/libutil-c/nix_api_util.cc Outdated
});
}

boost::container::small_vector<const nix_logger_field *, 1> cfields;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we bump the capacity to 4? That should be plenty and it's nothing in terms of stack space

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread src/libutil-c/nix_api_util.cc Outdated
if (!vtable.result)
return;

boost::container::small_vector<nix_logger_field, 1> storage;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here :)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread src/libutil-c/nix_api_util.cc Outdated
});
}

boost::container::small_vector<const nix_logger_field *, 1> cfields;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread src/libutil-c/nix_api_util.cc Outdated
if (!vtable.start_activity)
return;

boost::container::small_vector<nix_logger_field, 1> storage;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread src/libutil-c/nix_api_util.cc
Comment thread src/libutil-c/nix_api_util.cc Outdated
t = NIX_LOGGER_FIELD_TYPE_INT;
v.i = field.i;
} else {
continue;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be an unreachable instead? If this happens it's a bug certainly...

If we think about forward compatibility, it would be the C logger's implementation to ensure that it ignores unknown values if we ever add new ones.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@RossComputerGuy
Copy link
Copy Markdown
Member Author

I guess one open question is how / whether a logger can report errors? Should it be able to or should it just swallow all errors that it might encounter internally? I guess it would be good for robustness.

Shouldn't logEI or log handle the errors?

@RossComputerGuy RossComputerGuy force-pushed the upstream-nix-capi-logger branch from 93e3022 to 07886fb Compare May 14, 2026 18:41
@xokdvium
Copy link
Copy Markdown
Contributor

I meant the case of "nix sent garbage to the logger that it doesn't understand" and whether we need a way to provide a way to the logger to handle the case, or should it swallow all errors?

@RossComputerGuy
Copy link
Copy Markdown
Member Author

Hmm, I think logger problems should be treated as being a non-error. Usually with what I've seen, loggers will just try their best. I think we'd have it swallow the errors.

@xokdvium
Copy link
Copy Markdown
Contributor

Makes sense generally, though I was thinking of some sophisticated loggers that reimplemented nom that might want to do error reporting. Not sure it's worth the design complexity for the first iteration?

@RossComputerGuy
Copy link
Copy Markdown
Member Author

For a V1, loggers could use the user data stuff to push their logging errors into wherever it should go. I'm sure this will not be the first iteration. I was just hoping this would be enough for at least basic integration.

@xokdvium
Copy link
Copy Markdown
Contributor

Yup, we can always extend the vtable I think to add error handling if needed.

Copy link
Copy Markdown
Contributor

@xokdvium xokdvium left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hopefully the last pass of review. Looking almost perfect! Sorry for the tedious review 🥲

I guess another useful addition could be to have a way for the logger to ask nix to strip away ANSI escape sequences automatically, but that's probably good to leave for V2 iteration.

t = NIX_LOGGER_FIELD_TYPE_INT;
v.i = field.i;
} else {
std::unreachable();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh std::unreachable actually invoked UB. We have nix::unreachable to trigger std::terminate and crash though.

Comment on lines +123 to +153
boost::container::small_vector<nix_logger_field, 4> storage;
storage.reserve(fields.size());
for (const auto & field : fields) {
nix_logger_field_type t;
nix_logger_field_value v{};
if (field.type == nix::Logger::Field::tString) {
t = NIX_LOGGER_FIELD_TYPE_STR;

nix_logger_field_value_string str;
str.value = field.s.data();
str.len = field.s.size();

v.str = str;
} else if (field.type == nix::Logger::Field::tInt) {
t = NIX_LOGGER_FIELD_TYPE_INT;
v.i = field.i;
} else {
std::unreachable();
}

storage.push_back(
nix_logger_field{
.type = t,
.value = v,
});
}

boost::container::small_vector<const nix_logger_field *, 4> cfields;
cfields.reserve(storage.size());
for (const auto & f : storage)
cfields.push_back(&f);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be factored out into a helper function, right? There are 2 places that do this now.

* @note Must be kept in sync with `nix::ActivityType`.
*/
enum nix_activity_type {
NIX_ACTIVITY_TYPE_UNKNOWN = 0,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe NONE would be more in-line with what's documented above?

Comment on lines +101 to +108
std::vector<std::tuple<
nix_activity_id,
nix_verbosity,
nix_activity_type,
std::string,
std::vector<CapturedField>,
nix_activity_id>>
starts;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this maybe just be a struct? A bunch of std::get calls with indices aren't very readable.

Comment on lines +564 to +567
* @brief Replace the global Nix logger with one driven by C callbacks.
*
* After this call, log messages, activities and supported results
* produced anywhere in Nix are routed to the supplied callbacks.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another cool addition would be to have a way to get the old logger back or to chain the logger (like the json one is doing, I presume that for detnixd). Also not needed for the initial implementation though :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c api Nix as a C library with a stable interface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

C API: Logging callbacks

2 participants