Re: AV API thoughts: AVs as proxies for other things

[email protected] (Michael Conrad) Fri, 12 Jun 2026 14:30:39 -0400
Newsgroups perl.perl5.porters
Message-ID <[email protected]>
On 6/11/26 8:40 PM, Tony Cook wrote:
> On Thu, Jun 11, 2026 at 11:31:21AM +0100, Paul "LeoNerd" Evans wrote:
>>    https://github.com/Perl/perl5/pull/24451
>>
>> The currently-last comment in that PR I liked above, is some thinking
>> out loud on the complicated problem with the C-level API it proposes.
>> It observes that if at the C level you call `av_splice(...)` on a given
>> AV that is tied, there's no guarantee that the Perl `SPLICE` method that
>> gets invoked returns the right number of deleted items. In particular,
>> it could return more, which would be too many items for the caller to
>> account for in the C-level array it passed in for the purpose. Much
>> trouble. My comment ends with the observation that perhaps a workable
>> API would be to have three variants of the function, which differ in
>> the way they pass in or out a list (sequence?) of SVs for the splice
>> operation. The three variants could use C-level arrays of SV pointers,
>> perl's AV structures, or the perl data stack.
>>
> ...
>> As I said at the top, this isn't a fully-formed plan, or a set of
>> questions. I just wanted to write it out while it was in my head, for
>> future reference in case someone else came to similar ideas before, or
>> in future looking back at this.
> My initial thought thinking over this was that for inputs the (SV**,
> size_t) pair is sufficient, if the caller has a (non-tied) AV they can
> pass (AvARRAY(av), av_count(av)), if they have a local C array they
> can populate that, if the SVs are on the stack they can pass (MARK,
> (PL_stack_sp-MARK)).
>
> But Michael's comment on exceptions vs warnings reminded me that we
> need to deal with exceptions - if we support tied targets how do we
> deal with cleanup of these objects on an exception?
>
> For the stack it's no big deal, the objects can be mortal
> (non-RC_STACK) or owned by the stack (RC_STACK) and are cleaned up on
> rewind, but the others are more messy.  So perhaps your proxies are
> the right way to do it.
>
> Returning stuff is harder as we found.
>
> One option might be the caller supplies an AV and an offset, the API
> extends the AV as needed and adds the elements to the AV, which should
> cover returning on the stack too. (though we'd need to check something
> like (av == PL_curstack && rpp_stack_is_rc()) to decide on reference
> counting handling).
After trying to reply to this a few times with contradictory 
conclusions, I think I've finally arrived at an answer I'm comfortable 
with, and which doesn't require a pseudo-AV concept and doesn't have any 
unnecessary refcount changes to the array elements.

If the caller supplies SV to be copied (sv_setsv) into the array, the 
references must already be mortal or otherwise-owned; the normal XS 
conditions when calling any api that could croak.  Likewise, if the 
caller supplies SV to be added as-is with an additional reference added, 
they must already be mortal or otherwise-owned.  If the caller supplies 
SV that are "live references", the av_splice function must be 
responsible for cleaning them up.

Non-tied AV, Caller supplies SV* to be copied:

   - Create a temporary mortal AV, and populate it with SV copies of the 
supplied SVs.  sv_setsv can die, but this is ok, because the mortal AV 
cleans up the progress so far.
   - The temporary AV is no worse efficiency than if the caller had done 
all the sv_setsv, because they would also need something to own the 
copies until it was known that all copying succeeded.
   - If caller wants mortal references of removed elements, swap 
pointers between AVs.  The removed elements are mortal by virtue of 
being owned by a mortal AV.
   - If caller wants to claim references of removed elements, move 
pointers from target AV to return buffer and move pointers from temp AV 
to target AV

Non-tied AV, Caller supplies SV* to be referenced:
(never dies)

   - If caller wants mortal references, copy out with sv_2mortal
   - If caller wants to claim references, move pointers from AV to 
return buffer
   - Copy SV* into AV, increasing refcount of each

Non-tied AV, Caller supplies SV* to be claimed:
(never dies)

   - If caller wants mortal references, copy out with sv_2mortal
   - If caller wants to claim references, move pointers from AV to 
return buffer
   - Copy SV* into AV

Tied AV, Caller supplies SV* to be copied or referenced:

   - SV* get added to the stack, and SPLICE is called.  On RC_STACK, 
refcounts get incremented.
   - No need to trap exceptions, let stack cleanup handle it
   - If caller wants removed items returned, transfer from the stack

Tied AV, Caller supplies SV* to be claimed:

   - SV* get added to the stack, and SPLICE is called.  On RC_STACK, 
stack is new owner of supplied SVs.  On non-RC stack, sv_2mortal gets 
called on each element.
   - No need to trap exceptions, let stack cleanup handle it
   - If caller wants removed items returned, transfer from the stack

The only real inefficiency here is if the intended return buffer is the 
stack.  The API I originally proposed for av_splice requires the caller 
to have pre-allocated SV** storage for the removed elements, but that 
storage could end up occupied by elements in the return value of tied 
SPLICE, and ideally could just be left on the stack instead of popping 
them off and then pushing them back on.  I have no idea whether "return 
values in an AV which can be a virtual AV backed by the stack" would be 
more or less effort than another flag to av_splice that says "leave 
results on the stack".

-Mike C.