cxx-ada-derived-virtual-slots

C++ Ada derived virtual slots

Class-specific Ada destructor names consume new dispatch slots in derived classes and displace virtual methods introduced after them from their C++ vtable indices.

StagedApplies in patchset orderStaged in 1.2.0

Why this is staged.

Stabilizing destructor identities changes generated vtable slot naming for every derived class, which only has an observable contract once the ABI-layout bundles are accepted. Held out of the published patchset for separate upstream review.

Depends on

Where it applies.

How each patchset treats this bundle on each GCC major
PatchsetGCC 13GCC 14GCC 15GCC 16
1.2.0 (latest)Staged13.2.0Staged14.2.0Staged15.3.0Staged16.2.0
1.1.0Not applicable13.2.0Not applicable14.2.0Not applicable15.3.0Not applicable16.2.0
1.0.1Not applicable13.2.0Not applicable14.2.0Not applicable15.3.0Not applicable16.1.0
1.0.0Not applicable13.2.0Not applicable14.2.0Not applicable15.3.0Not applicable16.1.0

Explanation.

A virtual introduced in a derived C++ class occupies the next primary-vtable slot after inherited virtuals. The Ada declarations must assign the same slot to make a class-wide call through the generated binding.

The offending case is ordinary single inheritance; multiple inheritance only made the failure easier to expose:

class Base
{
public:
  virtual ~Base () = default;
  virtual int inherited_slot ();
protected:
  int base;
  int reserved;
};

class Derived : public Base
{
public:
  ~Derived () override = default;
  int inherited_slot () override;
  virtual int added_slot ();
private:
  int own;
};

The reserved field makes the base consume its complete-object storage, keeping this regression focused on virtual-slot identity rather than the separate tail-padding representation problem.

The unpatched mapper produces class-specific Ada destructor names:

package Class_Base is
   procedure Delete_Base (this : access Base);
   procedure Delete_And_Free_Base (this : access Base);
   function inherited_slot (this : access Base) return int;
end;

package Class_Derived is
   procedure Delete_Derived (this : access Derived);
   procedure Delete_And_Free_Derived (this : access Derived);
   function inherited_slot (this : access Derived) return int;
   function added_slot (this : access Derived) return int;
end;

Ada treats both derived destructor declarations as new primitives, not overrides. added_slot is assigned two entries later than its C++ slot. In the regression, the generated class-wide call loads RTTI instead of a function pointer and terminates abnormally.

The corrected mapper produces hierarchy-stable destructor names, so the derived declarations override their inherited slots:

package Class_Base is
   procedure Delete (this : access Base);
   procedure Delete_And_Free (this : access Base);
   function inherited_slot (this : access Base) return int;
end;

package Class_Derived is
   procedure Delete (this : access Derived'Class);
   procedure Delete_And_Free (this : access Derived'Class);
   function inherited_slot (this : access Derived) return int;
   function added_slot (this : access Derived) return int;
end;

The class-wide derived destructor form deliberately keeps those direct imports out of the derived type's primitive set. The two root destructor primitives already reserve the C++ destructor slots; dispatch through those inherited slots reaches the derived functions in the object's real C++ vtable. This leaves added_slot at the next C++ slot on every target instead of relying on Ada override recognition for compiler-generated destructor declarations.

A user method named Delete or Delete_And_Free would then collide with the generated name. The patch gives only such a method a _Method suffix while retaining its original C++ external symbol:

function Delete_Method (this : access Base'Class) return int
with Import => True,
     Convention => CPP,
     External_Name => "_ZN4Base6DeleteEv";

The executable regression reinterprets the C++ object pointer as an Ada class-wide access value, then proves inherited and newly introduced dispatch at -O0 and -O2. The unchecked pointer view is intentional: C++ RTTI is not an Ada tag descriptor and therefore cannot support Ada's checked class-membership conversion. The dispatch itself still reads the C++ object's real vtable.

The feature panel separately nests a concrete secondary base that itself has primary and secondary concrete bases. That case demonstrates the representation strategy: primary bases use Ada inheritance, secondary bases use nested exact-layout components, and an access view at each nested component address supplies the appropriate C++ dispatch-table view.

Patch.

Variant gcc-13-14

Applies to 13.2.0, 14.2.0. Source flavors: linux, darwin_arm64.

+74 −9 2 files

Download the patch

gcc/c-family/c-ada-spec.cc +34−9modified
Unified diff for gcc/c-family/c-ada-spec.cc: original line, patched line, change, source
@@ -1591 +1591 @@dump_ada_function_declaration (pretty_printer *buffer, tree func,
15911591 tree t;
15921592 char buf[18];
15931593 int num, num_args = 0, have_args = true, have_ellipsis = false;
1594Added line. bool is_derived_destructor = false;
1595Added line.
1596Added line. if (is_destructor)
1597Added line. for (tree field = TYPE_FIELDS (DECL_CONTEXT (func));
1598Added line. field;
1599Added line. field = TREE_CHAIN (field))
1600Added line. if (cpp_check && cpp_check (field, IS_PRIMARY_BASE_FIELD)
1601Added line. && is_tagged_type (TREE_TYPE (field)))
1602Added line. {
1603Added line. is_derived_destructor = true;
1604Added line. break;
1605Added line. }
15941606
15951607 /* Compute number of arguments. */
15961608 if (arg)
@@ -1686 +1698 @@dump_ada_function_declaration (pretty_printer *buffer, tree func,
1686Removed line. && !(num == 1 && is_method && (DECL_VINDEX (func) || is_constructor)))
1698Added line. && !(num == 1 && is_method
1699Added line. && (DECL_VINDEX (func) || is_constructor)
1700Added line. && !is_derived_destructor))
16871701 pp_string (buffer, "'Class");
@@ -3591 +3591 @@
35913591/* Dump in BUFFER destructor spec corresponding to T. */
35923592
35933593static void
3594Removed line. print_destructor (pretty_printer *buffer, tree t, tree type)
3594Added line. print_destructor (pretty_printer *buffer, tree t)
35953595{
3596Removed line. tree decl_name = DECL_NAME (TYPE_NAME (type));
3597Removed line.
3598Removed line. pp_string (buffer, "Delete_");
35993596 if (startswith (IDENTIFIER_POINTER (DECL_NAME (t)), "__dt_del"))
3600Removed line. pp_string (buffer, "And_Free_");
3601Removed line. pp_ada_tree_identifier (buffer, decl_name, t, false);
3597Added line. pp_string (buffer, "Delete_And_Free");
3598Added line. else
3599Added line. pp_string (buffer, "Delete");
36023600}
36033601
36043602/* Dump in BUFFER assignment operator spec corresponding to T. */
@@ -3653 +3651 @@
36533651 return false;
36543652}
36553653
3654Added line. /* Return true if METHOD_NAME would collide with a stable generated C++
3655Added line. destructor name. These names must remain identical throughout a class
3656Added line. hierarchy so Ada assigns overriding destructors to their C++ vtable slots. */
3657Added line.
3658Added line. static bool
3659Added line. method_name_matches_destructor (tree method_name)
3660Added line. {
3661Added line. const char *name = IDENTIFIER_POINTER (method_name);
3662Added line. return !strcasecmp (name, "Delete")
3663Added line. || !strcasecmp (name, "Delete_And_Free");
3664Added line. }
3665Added line.
36563666/* Return the name of type T. */
36573667
36583668static const char *
@@ -3987 +3997 @@
39873997 if (is_constructor)
39883998 print_constructor (buffer, t, type);
39893999 else if (is_destructor)
3990Removed line. print_destructor (buffer, t, type);
4000Added line. print_destructor (buffer, t);
39914001 else if (is_assignment_operator)
39924002 print_assignment_operator (buffer, t, type);
39934003 else
@@ -3997 +4007 @@
39974007 if (is_method)
39984008 {
39994009 print_method_qualifiers (buffer, t);
4000Removed line. if (method_name_matches_visible_type (decl_name, type))
4010Added line. if (method_name_matches_visible_type (decl_name, type)
4011Added line. || method_name_matches_destructor (decl_name))
40014012 pp_string (buffer, "_Method");
40024013 }
40034014 if (suffix > 1)
gcc/testsuite/g++.dg/ada-spec/derived-virtual-slots.C +40−0new file
Unified diff for gcc/testsuite/g++.dg/ada-spec/derived-virtual-slots.C: original line, patched line, change, source
@@ -0 +1 @@
1Added line. /* { dg-do compile } */
2Added line. /* { dg-options "-fdump-ada-spec-slim" } */
3Added line. /* { dg-final { scan-file derived_virtual_slots_c.ads "procedure Delete \(this : access Derived'Class\)" } } */
4Added line. /* { dg-final { scan-file derived_virtual_slots_c.ads "procedure Delete_And_Free \(this : access Derived'Class\)" } } */
5Added line. /* { dg-final { scan-file derived_virtual_slots_c.ads "function Delete_Method \(this : access Base'Class\)" } } */
6Added line.
7Added line. class Base
8Added line. {
9Added line. public:
10Added line. Base (int base_value);
11Added line. virtual ~Base () = default;
12Added line. virtual int inherited_slot () { return base; }
13Added line. int Delete () { return base; }
14Added line. int base;
15Added line. int reserved;
16Added line. };
17Added line.
18Added line. class Derived : public Base
19Added line. {
20Added line. public:
21Added line. Derived (int base_value, int own_value);
22Added line. ~Derived () override = default;
23Added line. int inherited_slot () override { return base + own; }
24Added line. virtual int added_slot () { return base + own + 5; }
25Added line. int own;
26Added line. };
27Added line.
28Added line. Base::Base (int base_value) : base (base_value), reserved (0) {}
29Added line. Derived::Derived (int base_value, int own_value)
30Added line. : Base (base_value), own (own_value) {}
31Added line.
32Added line. extern "C" Derived *create_derived () { return new Derived (10, 2); }
33Added line. extern "C" void delete_derived (Derived *object) { delete object; }
34Added line. extern "C" int cpp_call_inherited (Derived *object)
35Added line. {
36Added line. return object->inherited_slot ();
37Added line. }
38Added line. extern "C" int cpp_call_added (Derived *object) { return object->added_slot (); }
39Added line.
40Added line. /* { dg-final { cleanup-ada-spec } } */
41

Variant gcc-15-16

Applies to 15.3.0, 16.1.0, 16.2.0. Source flavors: linux, darwin_arm64.

+74 −9 2 files

Download the patch

gcc/c-family/c-ada-spec.cc +34−9modified
Unified diff for gcc/c-family/c-ada-spec.cc: original line, patched line, change, source
@@ -1645 +1645 @@dump_ada_function_declaration (pretty_printer *pp, tree func,
16451645 tree t;
16461646 char buf[18];
16471647 int num, num_args = 0, have_args = true, have_ellipsis = false;
1648Added line. bool is_derived_destructor = false;
1649Added line.
1650Added line. if (is_destructor)
1651Added line. for (tree field = TYPE_FIELDS (DECL_CONTEXT (func));
1652Added line. field;
1653Added line. field = TREE_CHAIN (field))
1654Added line. if (cpp_check && cpp_check (field, IS_PRIMARY_BASE_FIELD)
1655Added line. && is_tagged_type (TREE_TYPE (field)))
1656Added line. {
1657Added line. is_derived_destructor = true;
1658Added line. break;
1659Added line. }
16481660
16491661 /* Compute number of arguments. */
16501662 if (arg)
@@ -1740 +1752 @@dump_ada_function_declaration (pretty_printer *pp, tree func,
1740Removed line. && !(num == 1 && is_method && (DECL_VINDEX (func) || is_constructor)))
1752Added line. && !(num == 1 && is_method
1753Added line. && (DECL_VINDEX (func) || is_constructor)
1754Added line. && !is_derived_destructor))
17411755 pp_string (pp, "'Class");
@@ -3655 +3655 @@
36553655/* Dump in PP destructor spec corresponding to T. */
36563656
36573657static void
3658Removed line. print_destructor (pretty_printer *pp, tree t, tree type)
3658Added line. print_destructor (pretty_printer *pp, tree t)
36593659{
3660Removed line. tree decl_name = DECL_NAME (TYPE_NAME (type));
3661Removed line.
3662Removed line. pp_string (pp, "Delete_");
36633660 if (startswith (IDENTIFIER_POINTER (DECL_NAME (t)), "__dt_del"))
3664Removed line. pp_string (pp, "And_Free_");
3665Removed line. pp_ada_tree_identifier (pp, decl_name, t, false);
3661Added line. pp_string (pp, "Delete_And_Free");
3662Added line. else
3663Added line. pp_string (pp, "Delete");
36663664}
36673665
36683666/* Dump in PP assignment operator spec corresponding to T. */
@@ -3717 +3715 @@
37173715 return false;
37183716}
37193717
3718Added line. /* Return true if METHOD_NAME would collide with a stable generated C++
3719Added line. destructor name. These names must remain identical throughout a class
3720Added line. hierarchy so Ada assigns overriding destructors to their C++ vtable slots. */
3721Added line.
3722Added line. static bool
3723Added line. method_name_matches_destructor (tree method_name)
3724Added line. {
3725Added line. const char *name = IDENTIFIER_POINTER (method_name);
3726Added line. return !strcasecmp (name, "Delete")
3727Added line. || !strcasecmp (name, "Delete_And_Free");
3728Added line. }
3729Added line.
37203730/* Return the name of type T. */
37213731
37223732static const char *
@@ -4051 +4061 @@
40514061 if (is_constructor)
40524062 print_constructor (pp, t, type);
40534063 else if (is_destructor)
4054Removed line. print_destructor (pp, t, type);
4064Added line. print_destructor (pp, t);
40554065 else if (is_assignment_operator)
40564066 print_assignment_operator (pp, t, type);
40574067 else
@@ -4061 +4071 @@
40614071 if (is_method)
40624072 {
40634073 print_method_qualifiers (pp, t);
4064Removed line. if (method_name_matches_visible_type (decl_name, type))
4074Added line. if (method_name_matches_visible_type (decl_name, type)
4075Added line. || method_name_matches_destructor (decl_name))
40654076 pp_string (pp, "_Method");
40664077 }
40674078 if (suffix > 1)
gcc/testsuite/g++.dg/ada-spec/derived-virtual-slots.C +40−0new file
Unified diff for gcc/testsuite/g++.dg/ada-spec/derived-virtual-slots.C: original line, patched line, change, source
@@ -0 +1 @@
1Added line. /* { dg-do compile } */
2Added line. /* { dg-options "-fdump-ada-spec-slim" } */
3Added line. /* { dg-final { scan-file derived_virtual_slots_c.ads "procedure Delete \(this : access Derived'Class\)" } } */
4Added line. /* { dg-final { scan-file derived_virtual_slots_c.ads "procedure Delete_And_Free \(this : access Derived'Class\)" } } */
5Added line. /* { dg-final { scan-file derived_virtual_slots_c.ads "function Delete_Method \(this : access Base'Class\)" } } */
6Added line.
7Added line. class Base
8Added line. {
9Added line. public:
10Added line. Base (int base_value);
11Added line. virtual ~Base () = default;
12Added line. virtual int inherited_slot () { return base; }
13Added line. int Delete () { return base; }
14Added line. int base;
15Added line. int reserved;
16Added line. };
17Added line.
18Added line. class Derived : public Base
19Added line. {
20Added line. public:
21Added line. Derived (int base_value, int own_value);
22Added line. ~Derived () override = default;
23Added line. int inherited_slot () override { return base + own; }
24Added line. virtual int added_slot () { return base + own + 5; }
25Added line. int own;
26Added line. };
27Added line.
28Added line. Base::Base (int base_value) : base (base_value), reserved (0) {}
29Added line. Derived::Derived (int base_value, int own_value)
30Added line. : Base (base_value), own (own_value) {}
31Added line.
32Added line. extern "C" Derived *create_derived () { return new Derived (10, 2); }
33Added line. extern "C" void delete_derived (Derived *object) { delete object; }
34Added line. extern "C" int cpp_call_inherited (Derived *object)
35Added line. {
36Added line. return object->inherited_slot ();
37Added line. }
38Added line. extern "C" int cpp_call_added (Derived *object) { return object->added_slot (); }
39Added line.
40Added line. /* { dg-final { cleanup-ada-spec } } */
41

Tests.

derived-virtual-slots.C C++ · 40 lines
/* { dg-do compile } */
/* { dg-options "-fdump-ada-spec-slim" } */
/* { dg-final { scan-file derived_virtual_slots_c.ads "procedure Delete \(this : access Derived'Class\)" } } */
/* { dg-final { scan-file derived_virtual_slots_c.ads "procedure Delete_And_Free \(this : access Derived'Class\)" } } */
/* { dg-final { scan-file derived_virtual_slots_c.ads "function Delete_Method \(this : access Base'Class\)" } } */

class Base
{
public:
  Base (int base_value);
  virtual ~Base () = default;
  virtual int inherited_slot () { return base; }
  int Delete () { return base; }
  int base;
  int reserved;
};

class Derived : public Base
{
public:
  Derived (int base_value, int own_value);
  ~Derived () override = default;
  int inherited_slot () override { return base + own; }
  virtual int added_slot () { return base + own + 5; }
  int own;
};

Base::Base (int base_value) : base (base_value), reserved (0) {}
Derived::Derived (int base_value, int own_value)
  : Base (base_value), own (own_value) {}

extern "C" Derived *create_derived () { return new Derived (10, 2); }
extern "C" void delete_derived (Derived *object) { delete object; }
extern "C" int cpp_call_inherited (Derived *object)
{
  return object->inherited_slot ();
}
extern "C" int cpp_call_added (Derived *object) { return object->added_slot (); }

/* { dg-final { cleanup-ada-spec } } */

Download · View in repository

derived_virtual_slots_consumer.adb Ada · 42 lines
with Ada.Text_IO;
with Ada.Unchecked_Conversion;
with Derived_Virtual_Slots_C;
with Interfaces.C; use Interfaces.C;

procedure Derived_Virtual_Slots_Consumer is
   package Bindings renames Derived_Virtual_Slots_C;
   type Derived_Access is access all Bindings.Class_Derived.Derived;
   type Derived_Class_Access is
     access all Bindings.Class_Derived.Derived'Class;
   function To_Class is new Ada.Unchecked_Conversion
     (Derived_Access, Derived_Class_Access);

   Object : constant Derived_Access :=
     Derived_Access (Bindings.create_derived);
   View : constant Derived_Class_Access := To_Class (Object);
begin
   if Object = null then
      raise Program_Error with "C++ factory returned null";
   end if;

   Ada.Text_IO.Put_Line ("CHECK Ada inherited slot");
   Ada.Text_IO.Flush (Ada.Text_IO.Standard_Output);
   if Bindings.Class_Derived.inherited_slot (View) /= 12 then
      raise Program_Error with "Ada inherited virtual dispatch differs";
   end if;

   Ada.Text_IO.Put_Line ("CHECK Ada added slot");
   Ada.Text_IO.Flush (Ada.Text_IO.Standard_Output);
   if Bindings.Class_Derived.added_slot (View) /= 17 then
      raise Program_Error with "Ada added virtual dispatch differs";
   end if;

   if Bindings.cpp_call_inherited (Object) /= 12
     or else Bindings.cpp_call_added (Object) /= 17
   then
      raise Program_Error with "C++ virtual dispatch differs";
   end if;

   Bindings.delete_derived (Object);
   Ada.Text_IO.Put_Line ("MATCH derived virtual slots");
end Derived_Virtual_Slots_Consumer;

Download · View in repository

run-test.sh shell · 55 lines
#!/usr/bin/env bash
set -euo pipefail

if [[ $# -ne 3 ]]; then
  echo "usage: $0 TOOLCHAIN_ROOT GCC_VERSION unpatched|patched" >&2
  exit 2
fi

root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
source "$root/scripts/regression-common.sh"
resolve_regression_toolchain "$1"
version=$2
state=$3
[[ "$state" == unpatched || "$state" == patched ]] || exit 2
gxx="$REGRESSION_TOOLCHAIN/bin/g++"
cxx="$root/bundles/cxx-ada-derived-virtual-slots/tests/derived-virtual-slots.C"
ada="$root/bundles/cxx-ada-derived-virtual-slots/tests/derived_virtual_slots_consumer.adb"
work=$(mktemp -d "${TMPDIR:-/tmp}/gnat-cxx-ada-derived-slots.XXXXXX")
trap 'rm -rf "$work"' EXIT

for optimization in 0 2; do
  dir="$work/O$optimization"
  mkdir -p "$dir"
  cp "$cxx" "$ada" "$dir/"
  (
    cd "$dir"
    "${REGRESSION_ENV[@]}" "$gxx" -c "-O$optimization" \
      -fdump-ada-spec-slim derived-virtual-slots.C
    "${REGRESSION_ENV[@]}" "$REGRESSION_GNATMAKE" -q -f \
      "-O$optimization" derived_virtual_slots_consumer.adb \
      -largs derived-virtual-slots.o -lstdc++
  ) >"$dir/build.log" 2>&1 || { cat "$dir/build.log"; exit 1; }

  if [[ "$state" == unpatched ]]; then
    grep -F "procedure Delete_Derived" "$dir/derived_virtual_slots_c.ads"
    set +e
    (cd "$dir"; "${REGRESSION_ENV[@]}" ./derived_virtual_slots_consumer) \
      >"$dir/output.log" 2>&1
    status=$?
    set -e
    [[ $status -ne 0 ]] || {
      echo "error: unpatched derived virtual slot unexpectedly dispatched" >&2
      exit 1
    }
    echo "cxx-ada-derived-virtual-slots -O$optimization: expected bad dispatch (GCC $version)"
  else
    grep -F "procedure Delete (this : access Derived'Class)" "$dir/derived_virtual_slots_c.ads"
    grep -F "procedure Delete_And_Free (this : access Derived'Class)" "$dir/derived_virtual_slots_c.ads"
    grep -F "function Delete_Method (this : access Base'Class)" "$dir/derived_virtual_slots_c.ads"
    (cd "$dir"; "${REGRESSION_ENV[@]}" ./derived_virtual_slots_consumer) \
      >"$dir/output.log" 2>&1 || { cat "$dir/output.log"; exit 1; }
    grep -Fx "MATCH derived virtual slots" "$dir/output.log"
    echo "cxx-ada-derived-virtual-slots -O$optimization: patched (GCC $version)"
  fi
done

Download · View in repository

Commands.

Apply the patch
patch --fuzz=0 -p1 -i bundles/cxx-ada-derived-virtual-slots/patches/VARIANT.patch
Build the compiler
PATH=/path/to/bootstrap/bin:$PATH ./scripts/build-gnat.sh SOURCE BUILD INSTALL
Run the regression
./scripts/run-regressions.sh INSTALL 1.2.0 GCC_MAJOR patched

Metadata.