Skip to content

Add MAYBE modifier to ExtUtils::ParseXS - #24557

Open
Leont wants to merge 5 commits into
bleadfrom
parsexs-maybe
Open

Add MAYBE modifier to ExtUtils::ParseXS#24557
Leont wants to merge 5 commits into
bleadfrom
parsexs-maybe

Conversation

@Leont

@Leont Leont commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

As I suggested here before, I've added a MAYBE modifier to arguments and return values. This will map undef to/from a sentinel value such as NULL, but otherwise use the typemap as usual.

  • This set of changes does not require a perldelta entry, because ExtUtils::ParseXS has its own Changes file

@Leont
Leont requested review from iabyn and leonerd July 13, 2026 11:38
@Leont Leont added the defer-next-dev This PR should not be merged yet, but await the next development cycle label Jul 13, 2026

@leonerd leonerd left a comment

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.

Overall this feels good, but a few small comments about trying to be clearer in the docs / code.

Comment thread dist/ExtUtils-ParseXS/lib/ExtUtils/ParseXS/Node.pm Outdated
Comment thread dist/ExtUtils-ParseXS/lib/ExtUtils/ParseXS/Node.pm Outdated
Comment thread dist/ExtUtils-ParseXS/lib/ExtUtils/ParseXS/Node.pm Outdated
Comment thread dist/ExtUtils-ParseXS/lib/ExtUtils/ParseXS/Node.pm Outdated
Comment thread dist/ExtUtils-ParseXS/lib/perlxs.pod
Comment thread dist/ExtUtils-ParseXS/lib/ExtUtils/ParseXS/Node.pm Outdated
$if_null .= "\n\t\tSTRLEN_length_of_\$var = 0;" if $self->{length_param};

$init_template = <<END;
if (SvOK(\$arg)) {

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.

Nothing has called SvGETMAGIC() at this point, so SvOK() may not be valid (fixing that doesn't seem pretty, unless we accept extra magic calls.)

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.

That's a good one. I think I've handled it everywhere I can now. There's no SvRV_nomg so I guess it doesn't matter enough in that corner.

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.

The existing XS should[1] SvGETMAGIC() before SvROK(), SvRV() anyway, but you still need the SvGETMAGIC() before the SvOK().

If I fix T_PTRREF to properly SvGETMAGIC():

        [
            "test MAYBE PTRREF",
            Q(<<'EOF'),
                |TYPEMAP: <<EOF
                |myclass   T_MYPTRREF
                |INPUT
                |T_MYPTRREF
                |	SvGETMAGIC($arg);
                |	if (SvROK($arg)) {
                |	    IV tmp = SvIV((SV*)SvRV($arg));
                |	    $var = INT2PTR($type,tmp);
                |	}
                |	else
                |	    Perl_croak_nocontext("%s: %s is not a reference",
                |			${$ALIAS?\q[GvNAME(CvGV(cv))]:\qq["$pname"]},
                |			"$var")
                |EOF
                |
                |void
                |foo(MAYBE myclass input)
EOF
            [  0, qr/fail me/, 'I want to see the code' ],
        ],

We get:

# XS_EUPXS(XS_Foo_foo)
# {
#     dVAR; dXSARGS;
#     if (items != 1)
#        croak_xs_usage(cv,  "input");
#     {
#       myclass input;
# 
#       SvGETMAGIC(ST(0));
#       if (SvOK(ST(0))) {
#               SvGETMAGIC(ST(0));
#       if (SvROK(ST(0))) {
#           IV tmp = SvIV_nomg((SV*)SvRV(ST(0)));
#           input = INT2PTR(myclass,tmp);
#       }
#       else
#           Perl_croak_nocontext("%s: %s is not a reference",
#                       "Foo::foo",
#                       "input")
# ;
#       } else {
#               input = NULL;;
#       }
# ;
# 
#       foo(input);
#     }
#     XSRETURN_EMPTY;
# }'

Which has two problems:

  • an extraneous SvGETMAGIC(ST(0));
  • removed the magic from an SvIV() that wasn't ST(0)

I think it is safer to be extra magical just have extra magic calls.

[1] though lib/ExtUtils/typemap is painfully inconsistent about it, and thinking about this lead to tonycoz/imager#566

@Leont Leont Jul 21, 2026

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.

I hadn't realized SvRV never does SvGETMAGIC, that does change things. Now it's always added.

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.

If I modify a match for the "test MAYBE with length" test to fail I see:

# XS_EUPXS(XS_Foo_foo)
# {
#     dVAR; dXSARGS;
#     if (items != 1)
#        croak_xs_usage(cv,  "input");
#     {
#       STRLEN  STRLEN_length_of_input;
#       size_t  XSauto_length_of_input;
#       char*   input;
# 
#       SvGETMAGIC(ST(0));
#       if (SvOK(ST(0))) {
#               input = (char *)SvPV(ST(0), STRLEN_length_of_input)
# ;
#       } else {
#               input = NULL;
#               STRLEN_length_of_input = 0;;
#       }
# ;
# 
#       XSauto_length_of_input = STRLEN_length_of_input;
# 
#       foo(input, XSauto_length_of_input);
#     }
#     XSRETURN_EMPTY;
# }'

The code looks like you're trying to support this case, but matching against the closing ) means this still uses the magical SvPV() instead of the non-magical variant.

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.

though lib/ExtUtils/typemap is painfully inconsistent about it

Now #24589

Comment thread dist/ExtUtils-ParseXS/t/005-parse-parameters.t
@jkeenan jkeenan removed the defer-next-dev This PR should not be merged yet, but await the next development cycle label Jul 15, 2026
@Leont
Leont force-pushed the parsexs-maybe branch 3 times, most recently from 2bbac35 to 67abbe1 Compare July 20, 2026 19:23

$self->{no_output} = 1 if $type =~ s/^NO_OUTPUT\s+//;

$self->{retval_sentinel} = defined($1) ? $1 : 'NULL' if $type =~ s/^\s* MAYBE (?: \( (-? \w+) \) )? \s*//x;

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.

One thing that occurred to me this morning, was whether this should accept C++ namespace/class qualified names, ie. allow colons.

My ideal would be to accept an expression like MAYBE(Perl_nan()) or MAYBE(std::numeric_limits<NV>::quiet_NaN()) but I can see why you'd want to avoid that.

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.

One thing that occurred to me this morning, was whether this should accept C++ namespace/class qualified names, ie. allow colons.

I kind of doubt it does in practice, but we can always make it more permissive in the future.

@Leont
Leont force-pushed the parsexs-maybe branch 4 times, most recently from c5dfeff to 43c8fe7 Compare July 21, 2026 08:48
Comment thread dist/ExtUtils-ParseXS/lib/perlxs.pod Outdated

if ($init_template && defined $sentinel) {
$init_template =~ s/^(?!\A)/\t/g;
$init_template =~ s/(Sv(?:IV|UV|PV(?:|byte|utf8)?))(_nolen|)(\(\$arg\))/$1_nomg$2$3/;

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.

SvNV is missing and does have a _nomg variant.

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.

Something that occurred to me while I was out led to:

        [
            "test MAYBE byte",
            Q(<<'EOF'),
                |TYPEMAP: <<TM
                |BYTE * T_BYTEV
                |INPUT
                |T_BYTEV
                |    $var = ($type)SvPVbyte_nolen($arg);
                |TM
                |void
                |foobyte(MAYBE BYTE *input)
EOF
            [  0, qr/if \(SvOK\(ST\(0\)\)\)/, 'check for SvOK' ],
            [  0, qr/input = \(BYTE \*\)SvPVbyte_nomg_nolen\(ST\(0\)\)/ ],
            [  0, qr/xinput = NULL;/, 'falls back to setting to null'],
        ],

xinput to force a fail:

# XS_EUPXS(XS_Foo_foobyte)
# {
#     dVAR; dXSARGS;
#     if (items != 1)
#        croak_xs_usage(cv,  "input");
#     {
#       BYTE *  input;
# 
#       SvGETMAGIC(ST(0));
#       if (SvOK(ST(0))) {
#           input = (BYTE *)SvPVbyte_nomg_nolen(ST(0))
# ;
#       } else {
#               input = NULL;;
#       }
# ;
# 
#       foobyte(input);
#     }
#     XSRETURN_EMPTY;
# }'
#     doesn't match '(?^:xinput = NULL;)'

but:

tony@venus:.../git/perl6$ git grep SvPVbyte_nomg_nolen
dist/ExtUtils-ParseXS/t/005-parse-parameters.t:            [  0, qr/input = \(BYTE \*\)SvPVbyte_nomg_nolen\(ST\(0\)\)/ ],

there is no such thing as SvPVbyte_nomg_nolen.

My original thought was that not all the various combinations of SvPV(byte|utf8)?(_nomg)?(_nolen)? have existed over the history of perls that EU::PXS needs to support.

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.

SvNV is missing and does have a _nomg variant.

D'oh!

@Leont Leont Jul 22, 2026

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.

there is no such thing as SvPVbyte_nomg_nolen.

That is most annoying. I guess that needs to be SvPVbyte_nomg($arg, PL_na) then?

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.

SvTRUE() is also missing (encountered while looking for the versions below)

I guess that needs to be SvPVbyte_nomg($arg, PL_na) then?

That looks right, but...

My original thought was that not all the various combinations of SvPV(byte|utf8)?(_nomg)?(_nolen)? have existed over the history of perls that EU::PXS needs to support.

I think this is still an issue: the replacements need to be perl version dependent:

  • SvPV(byte|utf8)_nomg were added in 5.31.3 (757fc32).
  • SvNV_nomg() was added in 5.13.1 (6f1401d)
  • SvPV_nomg_nolen() was added in v5.13.6-122-g71eb6d8cfb
  • SvTRUE_nomg() was added in v5.13.5-120-g06c841cf64 (if you add SvTRUE())
  • Sv(IV|UV)_nomg() were added in perl-5.8.0-3191-g891f9566d7 (which probably isn't an issue)
  • SvPV_nomg was added in perl-5.6.0-3776-g8d6d96c1bf
  • SvPV_nomg() was added in perl-5.005_02-822-g1fa8b10d8c

(git grep -S foo is my friend)

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.

Alternatively MAYBE could reject old perls.

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.

Alternatively MAYBE could reject old perls.

Or just require ppport.h on older perls?

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.

Or just require ppport.h on older perls?

That's an option, though SvPVbyte_nomg() isn't defined by ppport.h, I haven't checked any others.

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.

Oh that's annoying. We may have to fix that. And in the mean time I guess it doesn't work on older perls.

Comment thread dist/ExtUtils-ParseXS/t/005-parse-parameters.t Outdated
@Leont
Leont force-pushed the parsexs-maybe branch 2 times, most recently from 0ad82b0 to 0a2a133 Compare July 22, 2026 13:24
Leont added 5 commits July 22, 2026 16:14
The code is currently put after short argument typemaps, but before long
ones. This was fine when STRLEN_length_of_* was only set by short ones,
but in the follow up commit it will be set by a longer one, so it needs
to be moved after the long ones.
This adds the MAYBE modifier to arguments. This will map undef to the
given sentinel (or NULL if none is given), and otherwise use the usual
typemapping machinery.
This adds the MAYBE modifier to return values and output arguments.
This will map undef to the given sentinel (or NULL if none is given),
and otherwise use the usual typemapping machinery.
| MAYBE char* foo()
EOF
[ 0, qr/if \(RETVAL == NULL\)/, '' ],
# [ 0, qr/asdf/, ''],

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.

left over from testing?

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.

Yeah, I'll remove that

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.

still there

typemap logic for any defined values. If an arguments it given to the C<MAYBE>
(e.g. C<MAYBE(-1)>) then that value is used as the sentinel, otherwise C<NULL>
is used.

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 only describes input parameters ("mapping undef to NULL" etc). It should at least briefly mention output parameters. The prose added further up seems to just be about return values.

@iabyn iabyn left a comment

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.

Apart from the PADTMP performance issue, all my comments are small niggles, and I'm otherwise generally happy with this PR.

# to change SvPV_nolen() to SvPV() or similar.
#
# The final assign should be deferred to come after all
# declarations.

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.

I'd prefer this chunk of code comments to be left where it is. Just above, it's explained what set of code lines should be produced for a length(s), and now these comments explain where some of those lines are generated. Just change the 'The final assign' bit to say that that line is added near the end of this function.

'is_synthetic', # Bool: var like 'THIS': we pretend it was in the sig

# values derived from both the XSUB's signature and/or INPUT line
'sentinel', # Str: The C constant expression to map undef to

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.

The 'sentinel' field line should be added 2 lines earlier, before the 'values derived from both the XSUB's signature and/or INPUT line' comment, since you can't have a MAYBE in an INPUT line.

],

[
"test MAYBE with length",

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.

Presumably "MAYBE length(s)" an error. If so, there should be a test for it.

}

return ($init_template, $eval_vars, 1);
return ($init_template, $eval_vars, $sentinel, 1);

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.

Since lookup_input_typemap() now returns an extra value, the code comments at the top of the function should be updated to reflect this. I.e. the 'Returns ($expr, $eval_vars, $is_template)' bit plus an extra line a few lines further down describing the new value.

SvPVutf8_nolen => 'SvPVutf8_nomg(%s, PL_na',
);
if ($init_template && defined $sentinel) {
$init_template =~ s/^(?!\A)/\t/g;

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.

I have no idea what the s/^(?!\A)/\t/g; is intended to do. Perhaps a comment at the end of it explaining it?

# SV * targ = (PL_op->op_private & OPpENTERSUB_HASTARG)
# ? PAD_SV(PL_op->op_targ) : sv_newmortal()

if ( $pxs->{config_optimize}

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 new section seems very sub-optimal. In the presence of a MAYBE return modifier, TARG is never used, and a so new mortal is created for each return (even if the sentinel value is never encountered).

[ 0, qr/sv_setsv\(ST\(0\), &PL_sv_undef\)/, ''],
],

[

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.

Really aught to have tests for OUTLIST and default parameters too.

which allow you to return Perl's true and false values, or to return
an empty list.

Since ExtUtils::ParseXS 3.64, you can also use the C<MAYBE> modifier to

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 new section doesn't actually say what the sentinel is for, or what values it can have. Perhaps something like:
If RETVAL is equal to the specified sentinel value (-1 in this example), then an undef-valued SV is returned instead. The sentinel is any simple C value. The value "NULL" is used if not specified.

of C<int> is only meaningful for declaring C<RETVAL> and for doing the
autocall.

It can also be prefixed with C<MAYBE> or C<MAYBE(sentinel)>. In such an XSUB,

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 section's title should be updated to include the 'MAYBE' word, so it's more likely to be found when looking at a contents page. e.g.

=head3 An XSUB's return type and the NO_OUTPUT and MAYBE keywords

The previous '=head2 An XSUB Declaration' list of examples also aught have the MAYBE keyword somewhere.

foo(int a, b, char *c)
C_ARGS: a, c

=head3 XSUB parameters with sentinels

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.

Again, this really needs 'MAYBE' in the section header, e.g.
=head3 MAYBE: XSUB parameters with sentinels.
This section also really needs a small code example.

@iabyn

iabyn commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

A few further comments: presumably a return type of 'MAYBE void' should give an error (and be tested for)?

The commit message for "ExtUtils::ParseXS: Add MAYBE support to return values and output" says "This will map undef to the given sentinel" which is the wrong way round for return/output values.

Should the docs mention that in the presence of length(), the length also gets set to 0 if the sentinel is triggered?

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants