Summary
When using f90wrap --direct-c, calling a wrapped Fortran subroutine with None
for an OPTIONAL scalar argument causes present(arg) to return .true. with
value 0 inside Fortran, instead of .false.. This breaks any code that tests
present() to decide whether to use a default value.
The plain f90wrap + f2py-f90wrap path handles this correctly.
Minimal reproducible example
Four files are needed. The Makefile has two targets that demonstrate both paths:
make test-cmake — builds via f90wrap --direct-c + CMake; bug present
make test-f2py — builds via plain f90wrap + f2py-f90wrap; works correctly
mymod.f90
module mymod
implicit none
contains
subroutine init(x, n)
real, intent(out) :: x
integer, intent(in), optional :: n
integer, parameter :: n_default = 42
if (.not. present(n)) then
x = real(n_default)
elseif (n >= 1) then
x = real(n)
else
print *, "Error: n must be >= 1"
print *, "n = ", n
error stop
end if
end subroutine
end module
test.py
import mymod
print("Calling explicit n:")
x2 = mymod.mymod.init(n=5)
print(f"Result with n=5: {x2}") # expected: 5.0
# Should use default (n=42) — but crashes with "Error: n must be >= 1"
print("Calling without explicit n:")
x = mymod.mymod.init()
print(f"Result: {x}") # expected: 42.0
CMakeLists.txt (used by make test-cmake to build the direct-C wrapper)
cmake_minimum_required(VERSION 3.18)
project(optional_bug_example Fortran C)
find_package(Python3 REQUIRED COMPONENTS Interpreter Development.Module NumPy)
find_program(F90WRAP_EXECUTABLE f90wrap REQUIRED)
set(SRC ${CMAKE_CURRENT_SOURCE_DIR}/mymod.f90)
add_custom_command(
OUTPUT
${CMAKE_CURRENT_BINARY_DIR}/f90wrap_mymod.f90
${CMAKE_CURRENT_BINARY_DIR}/_mymod.c
COMMAND ${F90WRAP_EXECUTABLE} --direct-c -m mymod ${SRC}
DEPENDS ${SRC}
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMENT "Generating f90wrap direct-C bindings"
VERBATIM
)
Python3_add_library(_mymod MODULE WITH_SOABI
${SRC}
${CMAKE_CURRENT_BINARY_DIR}/f90wrap_mymod.f90
${CMAKE_CURRENT_BINARY_DIR}/_mymod.c
)
target_include_directories(_mymod PRIVATE ${Python3_NumPy_INCLUDE_DIRS})
set_target_properties(_mymod PROPERTIES INSTALL_RPATH_USE_LINK_PATH TRUE)
Makefile
BUILDDIR_CMAKE := build_cmake
BUILDDIR_F2PY := build_f2py
.PHONY: all build-cmake build-f2py test-cmake test-f2py test clean
all: test-cmake
# direct-C build via CMake (bug IS present)
build-cmake:
cmake -B $(BUILDDIR_CMAKE) -S . -DCMAKE_BUILD_TYPE=Debug
cmake --build $(BUILDDIR_CMAKE)
cp $(BUILDDIR_CMAKE)/_mymod.cpython-*.so .
cp $(BUILDDIR_CMAKE)/mymod.py .
test-cmake: build-cmake
@echo "=== direct-C path (bug expected) ==="
python test.py; true
# f2py build (bug is NOT present)
build-f2py:
mkdir -p $(BUILDDIR_F2PY)
cd $(BUILDDIR_F2PY) && f90wrap -m mymod ../mymod.f90
cd $(BUILDDIR_F2PY) && f2py-f90wrap -c -m _mymod f90wrap_mymod.f90 ../mymod.f90
cp $(BUILDDIR_F2PY)/_mymod.cpython-*.so .
cp $(BUILDDIR_F2PY)/mymod.py .
test-f2py: build-f2py
@echo "=== f2py path (no bug expected) ==="
python test.py
test: test-cmake test-f2py
clean:
rm -f _mymod.cpython-*.so _mymod.so f90wrap_mymod.f90 _mymod.c *.o *.mod mymod.py
rm -rf $(BUILDDIR_CMAKE) $(BUILDDIR_F2PY)
make test output:
=== direct-C path (bug expected) ===
Calling explicit n:
Result with n=5: 5.0
Calling without explicit n:
Error: n must be >= 1
n = 0
ERROR STOP
=== f2py path (no bug expected) ===
Calling explicit n:
Result with n=5: 5.0
Calling without explicit n:
Result: 42.0
The direct-C path receives n = 0 and present(n) returns .true., while the
f2py path correctly receives a NULL pointer so present(n) returns .false..
Likely cause
f90wrap --direct-c generates a C wrapper (_mymod.c) where the None case for
an optional argument sets the value to 0 but leaves the pointer non-NULL:
/* generated by f90wrap --direct-c (buggy) */
int* n = &n_val;
if (py_n == Py_None) {
n_val = 0; /* pointer stays &n_val — Fortran sees present(n) == .true. */
} else {
...
}
F90WRAP_F_SYMBOL(f90wrap_mymod__init)(&x_val, n);
The Fortran binding wrapper generated by f90wrapgen.py declares n as
optional, so passing a NULL pointer would make present(n) return .false. —
but the C layer never passes NULL.
This is in f90wrap/directc_cgen/arguments_scalar.py,
prepare_scalar_argument. Compare with the f2py path in
f90wrap/scripts/f2py_f90wrap.py which correctly uses:
'#varname#_capi == Py_None ? NULL : &#varname#,'
Potential fix
In prepare_scalar_argument, set the pointer to NULL instead of the value to
0 when None is received:
# f90wrap/directc_cgen/arguments_scalar.py
if optional:
gen.write(f"if (py_{arg.name} == Py_None) {{")
gen.indent()
- gen.write(f"{arg.name}_val = 0;")
+ gen.write(f"{arg.name} = NULL;") # NULL → present() returns .false.
gen.dedent()
gen.write("} else {")
Environment
- f90wrap version: 0.3.0
- Python version: 3.13.5
- Compiler: GNU Fortran (Debian 14.2.0-19) 14.2.0 / GCC 14.2.0
Sidenote
I have attached a zip of the above example.
optional_bug_example.zip
As promised @krystophny, I would do my best in finding bugs for you.
Summary
When using
f90wrap --direct-c, calling a wrapped Fortran subroutine withNonefor an
OPTIONALscalar argument causespresent(arg)to return.true.withvalue
0inside Fortran, instead of.false.. This breaks any code that testspresent()to decide whether to use a default value.The plain
f90wrap+f2py-f90wrappath handles this correctly.Minimal reproducible example
Four files are needed. The
Makefilehas two targets that demonstrate both paths:make test-cmake— builds viaf90wrap --direct-c+ CMake; bug presentmake test-f2py— builds via plainf90wrap+f2py-f90wrap; works correctlymymod.f90module mymod implicit none contains subroutine init(x, n) real, intent(out) :: x integer, intent(in), optional :: n integer, parameter :: n_default = 42 if (.not. present(n)) then x = real(n_default) elseif (n >= 1) then x = real(n) else print *, "Error: n must be >= 1" print *, "n = ", n error stop end if end subroutine end moduletest.pyCMakeLists.txt(used bymake test-cmaketo build the direct-C wrapper)Makefilemake testoutput:The direct-C path receives
n = 0andpresent(n)returns.true., while thef2py path correctly receives a NULL pointer so
present(n)returns.false..Likely cause
f90wrap --direct-cgenerates a C wrapper (_mymod.c) where theNonecase foran optional argument sets the value to
0but leaves the pointer non-NULL:The Fortran binding wrapper generated by
f90wrapgen.pydeclaresnasoptional, so passing a NULL pointer would makepresent(n)return.false.—but the C layer never passes NULL.
This is in
f90wrap/directc_cgen/arguments_scalar.py,prepare_scalar_argument. Compare with the f2py path inf90wrap/scripts/f2py_f90wrap.pywhich correctly uses:'#varname#_capi == Py_None ? NULL : &#varname#,'Potential fix
In
prepare_scalar_argument, set the pointer toNULLinstead of the value to0whenNoneis received:Environment
Sidenote
I have attached a zip of the above example.
optional_bug_example.zip
As promised @krystophny, I would do my best in finding bugs for you.