Skip to content

fix: pass NULL for absent optional arguments in --direct-c#370

Open
krystophny wants to merge 3 commits into
jameskermode:masterfrom
krystophny:fix-issue-369-optional-directc
Open

fix: pass NULL for absent optional arguments in --direct-c#370
krystophny wants to merge 3 commits into
jameskermode:masterfrom
krystophny:fix-issue-369-optional-directc

Conversation

@krystophny

@krystophny krystophny commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #369.

With --direct-c, passing None for an OPTIONAL argument left a non-NULL pointer reaching Fortran, so present(arg) returned .true. and any default-value branch was skipped. Three argument forms were affected:

  • optional scalar: the pointer stayed &<arg>_val after the value was zeroed (the reported case)
  • optional character input: a blank buffer was allocated and passed
  • optional character output (out/inout): a blank buffer was allocated, so an omitted optional was seen as present

All now pass NULL, matching the f2py path (#var#_capi == Py_None ? NULL : &#var#). For optional character outputs, the return path yields None when the buffer is NULL.

Audit of the remaining optional kinds in directc_cgen: optional arrays already default the pointer to NULL and guard preparation with != Py_None, and optional derived types initialise the handle pointer to NULL and fill it only when not None. Both were already correct; the new tests lock that in.

Verification

examples/issue369_optional_present covers optional scalar, character input, and array arguments through both build paths (f2py and --direct-c). Optional character outputs use different calling conventions on the two backends, so they are covered by direct-C generation tests in test/test_directc.py instead.

Direct-C example, before the fix:

FAIL: test_scalar_absent_uses_default
AssertionError: 0.0 != 42.0
FAIL: test_char_absent_uses_default
AssertionError: '' != 'ABSENT'

After the fix:

  • examples/issue369_optional_present: 6 tests OK on both the f2py and --direct-c paths
  • test/test_directc.py: 32 tests OK, including the optional scalar / character input / character output NULL checks
  • full direct-C example suite: All tests passed (55 examples)

The direct-C wrapper kept a non-NULL pointer for an optional scalar set
to None (it only zeroed the value) and allocated a blank buffer for an
optional character input. Both made present(arg) return .true. inside
Fortran, so default-value branches never ran. Issue jameskermode#369.

Set the pointer to NULL in both cases, matching the f2py path. Optional
output characters still allocate their buffer.

Add examples/issue369_optional_present, covering optional scalar,
character, and array arguments through both the f2py and direct-C paths.
@krystophny

Copy link
Copy Markdown
Contributor Author

@jameskermode could you review this? @GeorgGrassler this fixes the optional-argument present() bug you reported in #369; could you test whether it resolves your case? The new examples/issue369_optional_present reproduces it and passes on both the f2py and --direct-c paths.

Optional intent(out)/inout character arguments still allocated a blank
buffer when None was passed, so present(arg) stayed .true. Pass NULL for
every absent optional character, and return None for a NULL buffer in the
output path.

The f2py and direct-C backends use different calling conventions for
optional character outputs, so this case cannot share the example
harness. Cover it with direct-C generation tests in test/test_directc.py,
alongside checks for the optional scalar and optional character input.
@jameskermode

Copy link
Copy Markdown
Owner

Thanks for the report and fix. Looks fine from a quick read, NULL rather than value 0 is of course the correct thing to pass for an absent optional arg. Will merge once @GeorgGrassler confirms the fix.

@@ -103,6 +103,9 @@ def prepare_scalar_argument(gen: 'DirectCGenerator', arg: ft.Argument, intent: s
gen.write(f"if (py_{arg.name} == Py_None) {{")
gen.indent()
gen.write(f"{arg.name}_val = 0;")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@krystophny @jameskermode yes it works now thanks. Curious about the still present {arg.name}_val = 0. Is this not now redundant?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch, you're right. The pointer is overwritten with NULL before the Fortran call, so _val = 0 was never read. Removed in c4b40af. The optional character-input branch was already clean (sets the pointer to NULL with no zeroing).

The None branch overwrites the argument pointer with NULL before the
Fortran call, so the preceding _val = 0 is never read. Drop it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: --direct-c passes 0 instead of absent pointer for optional scalar arguments

3 participants