Skip to content

Add OIIO_NODISCARD to ImageInput create/open/read methods#5111

Open
mvanhorn wants to merge 2 commits intoAcademySoftwareFoundation:mainfrom
mvanhorn:osc/4781-nodiscard-imageinput
Open

Add OIIO_NODISCARD to ImageInput create/open/read methods#5111
mvanhorn wants to merge 2 commits intoAcademySoftwareFoundation:mainfrom
mvanhorn:osc/4781-nodiscard-imageinput

Conversation

@mvanhorn
Copy link
Copy Markdown

Summary

Adds OIIO_NODISCARD to ImageInput::create(), open(), read_image(), and read_scanlines() methods. Callers ignoring return values from these methods now get a compiler warning.

Why this matters

As noted in #4781, it's easy to accidentally ignore the bool return from read_image() or open(). These methods return false on failure, and silently proceeding with bad data leads to hard-to-debug issues. create() returns a unique_ptr that should never be discarded.

Changes

  • src/include/OpenImageIO/imageio.h: Added OIIO_NODISCARD to 14 method declarations in the ImageInput class, covering create() (2 overloads), open() (4 overloads), read_image() (4 overloads), and read_scanlines() (4 overloads).

Scoped to ImageInput only per @lgritz's guidance to start with one class and iterate.

Testing

  • Header compiles (syntax validated)
  • No behavioral changes - annotation only

Partial fix for #4781

This contribution was developed with AI assistance (Claude Code).

@linux-foundation-easycla
Copy link
Copy Markdown

linux-foundation-easycla bot commented Mar 25, 2026

CLA Signed

The committers listed above are authorized under a signed CLA.

@mvanhorn mvanhorn force-pushed the osc/4781-nodiscard-imageinput branch from 6ac7875 to 50ed282 Compare March 25, 2026 17:07
@lgritz
Copy link
Copy Markdown
Collaborator

lgritz commented Mar 25, 2026

Did you not run the CI before submitting the PR? Many of the CI job variants are failing -- for the obvious reason that OIIO itself calls these functions, often without checking the return code (I know, bad), so those will now be errors and break our build. Changes to the function declarations to add these annotations must be accompanied by changes to the call sites where we have up until now neglected the return values.

That's part of why I wanted to go slowly, a few calls at a time, because we'll want to carefully review and ensure that all the places that we must add the error checks are handling those previously-unchecked errors sensibly, so we want small, easily reviewable PRs for that sweeping set of changes.

As for why some job variants pass and others fail, I suspect that is reflecting which compiler, version, and C++ standard they are using.

@lgritz
Copy link
Copy Markdown
Collaborator

lgritz commented Mar 25, 2026

I like the idea of migrating to heavy use of nodiscard, but I fear how much user code will have broken builds as a result (as our own was, revealed by the CI!). Maybe there's a way to gently transition people?

I think there are two classes of return values we may wish to annotate as "nodiscard":

  1. Cases where the whole point is to use the return value, and ignoring it would 100% mean a mistake on the user's part. An example would be ImageInput::create() -- if you don't capture the result, what the heck are you doing?

  2. Cases where the function performs some action and returns an error code, and while it's good practice to check every result for errors, failing to do so is a common mistake to make and may not be harmful in practice (though it could prevent you from catching legit errors that occur at runtime).

Definitely for case 1, we should not hesitate to annotate with OIIO_NODISCARD now.

But for case 2, I wonder what people think of the merits of creating a second macro OIIO_NODISCARD_ERROR, that is conditionally defined, maybe something like this (perhaps in platform.h):

    #ifndef OIIO_CHECK_DISCARDED_ERROR_RETURN
    #    define OIIO_CHECK_DISCARDED_ERROR_RETURN 0
    #endif
    #if OIIO_CHECK_DISCARDED_ERROR_RETURN
    #    define OIIO_NODISCARD_ERROR OIIO_NODISCARD
    #else
    #    define OIIO_NODISCARD_ERROR
    #endif

So what this would do is:

  • For now, by default, it allows user code to ignore error return codes.
  • Downstream/user code may build with -DOIIO_CHECK_DISCARDED_ERROR_RETURN=1 to force the full errors if they want. This will allow users to audit their code easily for this mistake, activating it at a time their convenience.
  • In a future release, we'll switch the default value from 0 to 1 so that it will catch all such errors unless a downstream project uses -DOIIO_CHECK_DISCARDED_ERROR_RETURN=0.
  • Eventually (a couple major releases later), maybe we will choose to remove the option entirely and always require errors to be checked.

So, for example, a method like ImageInput::read_scanline() would be annotated with OIIO_NODISCARD_ERROR rather than the less flexible OIIO_NODISCARD.

Opinions?

@lgritz
Copy link
Copy Markdown
Collaborator

lgritz commented Mar 25, 2026

As for why some job variants pass and others fail, I suspect that is reflecting which compiler, version, and C++ standard they are using.

It looks like clang is always flagging the nodiscard errors, but gcc never is. We should look into why this is the case -- I would have expected more recent gcc versions to also turn them into errors. Have we done something wrong, or perhaps is something else needed for clang?

@mvanhorn
Copy link
Copy Markdown
Author

You're right - I should have run CI before submitting. Apologies for the noise.

The two-tier approach makes sense. I'll rework this to:

  1. Hard [[nodiscard]] only on create() and open() - ignoring these is always a bug
  2. OIIO_NODISCARD_GENTLE (initially a no-op) for bool-returning methods like read_image() where ignoring is sloppy but not catastrophic
  3. Fix OIIO's own unchecked call sites so CI passes

On the clang vs gcc question: gcc only treats [[nodiscard]] violations as warnings (even with -Wall), while clang promotes them to errors when -Werror is active. OIIO's clang CI configs likely include -Werror, which explains why clang fails but gcc doesn't.

I'll push the reworked version with call site fixes shortly.

@lgritz
Copy link
Copy Markdown
Collaborator

lgritz commented Mar 25, 2026

I prefer OIIO_NODISCARD_ERROR rather than OIIO_NODISCARD_GENTLE, in case we later identify other cases (aside from error status returns) that we want to independently select as gentle or harsh.

@mvanhorn
Copy link
Copy Markdown
Author

Good call on OIIO_NODISCARD_ERROR - clearer that it's specific to error-status returns and leaves room for other nodiscard categories later. Will use that naming in the rework.

@lgritz
Copy link
Copy Markdown
Collaborator

lgritz commented Apr 4, 2026

I'll push the reworked version with call site fixes shortly.

Just a gentle reminder in case this slipped your mind. There is no rush, of course. I just wanted to make sure it wasn't a case of your already having done it but forgotten to re-push.

@mvanhorn mvanhorn force-pushed the osc/4781-nodiscard-imageinput branch from 50ed282 to b6cfc38 Compare April 5, 2026 05:19
@mvanhorn
Copy link
Copy Markdown
Author

mvanhorn commented Apr 5, 2026

Reworked in b6cfc38. Two-tier approach as discussed:

  • OIIO_NODISCARD (hard [[nodiscard]]) stays on create() and open()
  • New OIIO_NODISCARD_ERROR macro (no-op for now) on read_image() and read_scanlines(), so downstream callers aren't broken during the transition

Also fixed unchecked return values at three internal call sites: null check after create() in imageioplugin.cpp, open() return propagation in jpegoutput.cpp, and create/open error handling in py_imagebuf.cpp.

Used OIIO_NODISCARD_ERROR per your naming preference. When you're ready to enforce it, flipping the macro to [[nodiscard]] will surface remaining unchecked callers.

Per @lgritz feedback, use a two-tier approach:

- OIIO_NODISCARD (hard [[nodiscard]]) on create() and open(), where
  ignoring the return is always a bug.
- OIIO_NODISCARD_ERROR (initially a no-op) on read_image() and
  read_scanlines(), where ignoring is sloppy but not catastrophic.
  This allows a gentle transition for downstream callers.

Also fix unchecked return values at internal call sites:
- imageioplugin.cpp: add null check after ImageInput::create()
- jpegoutput.cpp: check open() return and propagate failure
- py_imagebuf.cpp: check create()/open() return before writing

Refs: AcademySoftwareFoundation#4781
Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
@mvanhorn mvanhorn force-pushed the osc/4781-nodiscard-imageinput branch from b6cfc38 to 5b166ef Compare April 5, 2026 05:45
Copy link
Copy Markdown
Collaborator

@lgritz lgritz left a comment

Choose a reason for hiding this comment

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

This is looking great. I made a few minor suggestions.

I see that there are still some tests failing, I don't quite understand why. I believe this is somehow changing control flow in such a way that it changes what errors are printed in some case?

Comment on lines +273 to +274
if (!out || !out->open("temp.png", altered_spec))
return py::none();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think this changes the behavior -- instead of returning an empty 'bytes' object, now it returns none.

auto inp = ImageInput::create(f.first);
lock.lock();
if (inp->supports("procedural"))
if (inp && inp->supports("procedural"))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Good catch here!

Comment on lines +585 to +586
if (!open(out_name, orig_out_spec))
return false;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It used to be that even if it fails, it would call close(), below (though erroneously returning 'true'). Can we guarantee that every possible failure of open() leaves it in a closed state? Or should we structure this more like

    bool ok = open(...);
    // the comments
    close();
    return ok;

and that would exactly replicate what we did before, except for correctly returning false upon failure to open?

Comment on lines +464 to +471
// OIIO_NODISCARD_ERROR is for functions returning error status (bool) where
// ignoring the return is a bad practice but not always catastrophic. This is
// initially a no-op to allow a gentle transition; it can later be defined as
// [[nodiscard]] once downstream callers have been updated.
#ifndef OIIO_NODISCARD_ERROR
# define OIIO_NODISCARD_ERROR /* nothing for now */
#endif

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Let's structure this a little differently, so that a simple on/off can turn on the right behavior and not just leave it to change this line for future versions:

Suggested change
// OIIO_NODISCARD_ERROR is for functions returning error status (bool) where
// ignoring the return is a bad practice but not always catastrophic. This is
// initially a no-op to allow a gentle transition; it can later be defined as
// [[nodiscard]] once downstream callers have been updated.
#ifndef OIIO_NODISCARD_ERROR
# define OIIO_NODISCARD_ERROR /* nothing for now */
#endif
// OIIO_NODISCARD_ERROR is for functions returning error status (bool) where
// ignoring the return is a bad practice but not always catastrophic.
//
// OIIO_NODISCARD_ERROR_ENABLE, if nonzero, enables OIIO_NODISCARD_ERROR to
// take effect. The default is to disable it for OIIO < 3.3, and enable it for
// OIIO >= 3.3. But `-DOIIO_NODISCARD_ERROR_ENABLE=...` can be used to
// override the default, for example to flag discarded errors in older
// versions of OIIO, or to disable the warnings in future versions of OIIO.
#ifndef OIIO_NODISCARD_ERROR_ENABLE
# if OIIO_VERSION_LESS(3, 3, 0)
# define OIIO_NODISCARD_ERROR_ENABLE 0 /* disable for now */
# else
# define OIIO_NODISCARD_ERROR_ENABLE 1 /* enable for OIIO >= 3.3 */
# endif
#endif
#if OIIO_NODISCARD_ERROR_DEFAULT
# define OIIO_NODISCARD_ERROR OIIO_NODISCARD
#else
# define OIIO_NODISCARD /* nothing */
#endif

@mvanhorn
Copy link
Copy Markdown
Author

mvanhorn commented Apr 6, 2026

Addressed in 3dafe19:

  • platform.h: Implemented the version-gated OIIO_NODISCARD_ERROR_ENABLE approach. Defaults to disabled for < 3.3, enabled for >= 3.3, overridable with -DOIIO_NODISCARD_ERROR_ENABLE=.... Fixed two small issues in the suggestion: the #if guard referenced _DEFAULT instead of _ENABLE, and the else branch defined OIIO_NODISCARD instead of OIIO_NODISCARD_ERROR.

  • py_imagebuf.cpp: Restored py::bytes() return on open failure instead of py::none(). That was the behavioral change causing issues.

  • jpegoutput.cpp: Restructured to bool ok = open(...); close(); return ok; so close() always runs regardless of open() result.

  • imageio.h: Ran clang-format on the NODISCARD_ERROR-annotated declarations.

Relying on CI for verification (C++ project, no local build). The test failures should resolve since the py_imagebuf behavioral change and the jpegoutput close-path change were the likely culprits.

…l changes

- platform.h: Replace simple no-op macro with version-gated
  OIIO_NODISCARD_ERROR_ENABLE that defaults to disabled for OIIO < 3.3
  and enabled for >= 3.3, overridable via -D flag
- py_imagebuf.cpp: Restore returning empty py::bytes() instead of
  py::none() on open failure to preserve backward compatibility
- jpegoutput.cpp: Restructure open/close flow to guarantee close()
  is always called even when open() fails, matching previous behavior
- imageio.h: Apply clang-format to NODISCARD_ERROR-annotated functions

Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
@mvanhorn mvanhorn force-pushed the osc/4781-nodiscard-imageinput branch from 3dafe19 to 0c93039 Compare April 6, 2026 15:30
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.

2 participants