Return Result with dlerror messages instead of Maybe#1
Conversation
open, get, and get-from-module now return (Result a String) where the error branch carries the actual OS error from dlerror(). This removes the need for callers to wrap results in Maybe.to-result with a generic message. Also fixes a latent double-free: dlerror() returns a pointer to a static buffer owned by the implementation, but was registered as returning String (heap-owned). Now registered as (Ptr CChar) with a copying wrapper via String.from-cstr. The rebind macro is updated to match on Result, and now prints the real dlerror message instead of a generic "Failed to find symbol". Breaking change: callers using Maybe.Just/Maybe.Nothing pattern matching on these functions must switch to Result.Success/Result.Error.
There was a problem hiding this comment.
Build & Tests
CI passes on both ubuntu-latest and macos-latest (lint, format check, doc generation).
Findings
The dlerror() fix is correct and important. The old code registered dlerror as (Fn [] String), which makes Carp treat the return value as a heap-owned string and free it on scope exit. Since C's dlerror() returns a pointer to a static buffer, that was a real double-free / use-after-free bug. The new approach — registering dlerror- as (Fn [] (Ptr CChar)) and wrapping with String.from-cstr — copies the static buffer into a heap-allocated string that Carp can safely own. This also fixes close, which calls dlerror on the error path.
NULL from dlerror() is not handled. POSIX specifies that dlerror() returns NULL when no error has occurred since initialization or since the last dlerror() call. In this code dlerror is only called after valid? returns false, so there should always be an error. But if the dl error was already consumed by an earlier dlerror() call, or if some platform quirk triggers it, dlerror- returns a null (Ptr CChar) and String.from-cstr will be called on NULL. Whether that segfaults depends on Carp's String.from-cstr implementation — worth verifying. A defensive wrapper like (if (null? (dlerror-)) @"unknown error" (String.from-cstr (dlerror-))) would be safer, though this would call dlerror() twice in the success case (which is fine, the second call just returns NULL). Up to you whether to address this now or leave it as a known edge.
close still returns (Maybe String). dynlib.carp:66 — close uses (Maybe.Just (dlerror)) / (Maybe.Nothing). The PR description says it "also fixes close" (the dlerror double-free, which is true), but close was not migrated to Result. This is arguably fine since close has different semantics (succeed-with-nothing vs fail-with-message), but it's inconsistent with the PR's stated goal. If the intention is to keep close on Maybe, mentioning that explicitly in the PR description would help.
README.md not updated. The README still shows the old @0.0.4 tag and Maybe-based API in both code examples (README.md:13-21, README.md:30-34). Since this is a breaking API change, the README should be updated to match.
Generated docs are stale. docs/DynLib.html still shows the old Maybe-based signatures and @0.0.4 in the module doc. CI runs carp -x gendocs.carp but doesn't commit the result. The docs in the repo will be wrong until regenerated.
Result usage and examples look correct. open, get, get-from-module consistently return (Result a String). Both examples are updated. The rebind macro correctly matches on Result.Error e / Result.Success s and prints the actual dlerror message.
Verdict: revise
The core dlerror fix is sound and the Maybe-to-Result migration is clean, but the README needs updating for the breaking API change, and the NULL-from-dlerror edge case is worth a look.
|
Addressed feedback from @carpentry-reviewer:
Re: |
There was a problem hiding this comment.
Build & Tests
CI failing on both ubuntu-latest and macos-latest. The failure is in the "Generate docs" step (carp -x gendocs.carp):
I couldn't find the symbol 'valid?' at line 40, column 12 in 'dynlib.carp'.
Maybe you forgot to define it? at dynlib.carp:38:3.
This is a definition ordering bug introduced by this PR.
Prior feedback
- First review flagged: NULL from
dlerror()not handled, README not updated, generated docs stale. All three were addressed in the follow-up commit. - The NULL guard (
valid?check), README update to@0.0.5, and doc regeneration are all present.
Findings
1. Definition ordering bug (dynlib.carp:38-42) — CI blocker
The new defn dlerror (line 38) references valid?, but valid? is only registered on line 42. In Carp, symbols must be defined before use within a module. The old code had register dlerror (just a declaration with no body to resolve), so valid? coming after it didn't matter. The new defn dlerror has a body that the compiler evaluates, hitting the unresolved valid?.
Fix: Move (hidden valid?) (register valid? (Fn [a] Bool) "DynLib_isvalid") above (private dlerror) (hidden dlerror) (defn dlerror ...).
2. dlerror NULL guard — correct
The dlerror wrapper (line 38-40) stores the (Ptr CChar) in err, checks (valid? err) (which is DynLib_isvalid, i.e., != NULL), and returns @"unknown error" on NULL. This avoids calling dlerror() twice (which would clear the error state). Correct approach. ✓
3. Result migration — correct
open, get, get-from-module all consistently return (Result a String). The rebind macro correctly matches Result.Error e / Result.Success s and prints the actual error. ✓
4. dlerror double-free fix — correct
Registering dlerror- as (Fn [] (Ptr CChar)) prevents Carp from freeing the static buffer returned by C's dlerror(). The String.from-cstr call copies it into a heap-owned string. This also fixes close's error path (line 68: (Maybe.Just (dlerror))). ✓
5. README and docs — updated
README shows @0.0.5, Result-based API in both examples, Result.unsafe-from-success in the unsafe example. Doc HTML updated with new sigs and descriptions. ✓
6. close left on Maybe — acknowledged and reasonable
close has different semantics (succeed-with-nothing, fail-with-message). Maybe fits. The comment in the PR thread explains the rationale.
Verdict: revise
The implementation is correct and all previous feedback is addressed, but CI is broken due to definition ordering. Move register valid? above defn dlerror and CI should pass.
|
Addressed the CI-blocking feedback from @carpentry-reviewer (second review):
All prior feedback (NULL guard, README update, docs regeneration) was already addressed in the previous commit. CI should pass now. |
There was a problem hiding this comment.
Build & Tests
Build: pass (type-checks with carp --check locally; CI passes on ubuntu-latest + macos-latest)
Tests: pass (gendocs runs clean, both CI jobs green)
Prior feedback
| Issue | Status |
|---|---|
NULL from dlerror() not handled |
Fixed ✓ (dynlib.carp:40-42: valid? guard, returns @"unknown error" on NULL) |
| README not updated | Fixed ✓ (shows @0.0.5, Result-based examples) |
| Generated docs stale | Fixed ✓ (signatures match new (Result a String) types) |
Definition ordering (valid? before defn dlerror) |
Fixed ✓ (dynlib.carp:36-37, before use at line 40) |
close left on Maybe |
Intentional, acknowledged ✓ |
All previously flagged issues are resolved.
Findings
No new issues found. Verified the following edge cases:
-
dlerror NULL guard (dynlib.carp:40-42):
valid?maps toDynLib_isvalid(dlfcn_helper.h:3-5,return a != NULL). Ifdlerror()returns NULL, falls through to@"unknown error". No segfault possible. -
Double-free fix (dynlib.carp:35-36): Registering as
(Ptr CChar)prevents Carp from freeing the static buffer.String.from-cstrcopies into a safe heap allocation. Correct fix. -
rebindmacro (dynlib.carp:70-76): Pattern variablesin(Result.Success s)correctly captures the function value;(Result.Error e)prints the real dlerror message viaIO.errorln. The ownedestring fromdlerroris valid for the arm's lifetime. -
closebenefits from dlerror fix: Line 68 calls(dlerror)which now uses the safe wrapper — the old double-free on close's error path is also fixed. -
Symbol ordering:
register valid?at line 36-37 precedesdefn dlerrorat line 40. Correct.
Verdict: merge
The core dlerror double-free fix is sound, the Maybe-to-Result migration is clean and consistent, all prior review feedback is addressed, and CI passes on both platforms. Ship it.
Summary
open,get, andget-from-modulenow return(Result a String)instead of(Maybe a). The error branch carries the actual OS error message fromdlerror(), so callers get useful diagnostics without needing to wrap inMaybe.to-resultwith a generic message string.What changed
open:(Maybe Lib)→(Result Lib String)get:(Maybe a)→(Result a String)get-from-module:(Maybe a)→(Result a String)rebindmacro: updated to match onResult.Success/Result.Error, now prints the realdlerrormessage instead of a generic "Failed to find symbol"dlerrorregistration: fixed a latent double-free —dlerror()returns a pointer to a static buffer that must not be freed, but was registered asString(heap-owned). Now registered as(Ptr CChar)with a copying wrapper viaString.from-cstr. This also fixesclose, which callsdlerroron the error path.Resultdirectly (simpler — no moreMaybe.to-resultwrappers)Breaking change
Callers matching on
Maybe.Just/Maybe.Nothingfrom these functions must switch toResult.Success/Result.Error.Before:
After:
Opened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.