Skip to content

Commit ea884d3

Browse files
committed
fix(core): MarshalByRefObject's constructor is protected, and GetLifetimeService returns (#2297)
Two of the ticket's three parts, under SA-8 and SA-9. The third is split out as #2374 because it is a VTABLE change, which SA-3 excludes explicitly. (1) The constructor is protected, matching .NET's 'protected MarshalByRefObject()' -- C# rejects the equivalent with CS0144 -- AND so are the copy and move members, closing the slicing route the review did not enumerate. Two in-repo construction sites migrated, in two different executables as the review predicted. (2) GetLifetimeService() is added and throws PlatformNotSupportedException with .NET's own sentence. Its absence turned an OBSERVABLE RUNTIME DIAGNOSTIC INTO A COMPILE ERROR at an unrelated place, which is the review's own point. It is NOT virtual in .NET, so adding it costs no vtable slot -- exactly why this half could land and the other could not. (3) InitializeLifetimeService() is deliberately still absent (#2374). .NET's is virtual, and this class already has a vtable, so adding it inserts a slot here and in both derived classes. IT MUST NOT BE ADDED NON-VIRTUALLY AS A WORKAROUND: a non-virtual member of that name would silently defeat the one thing .NET's exists for -- letting a derived type override the lease policy -- while looking correct at every call site. Its absence is pinned through a detection idiom, so #2374's arrival is visible rather than silent. MemberwiseClone(bool) stays absent: System::Object declares no MemberwiseClone, measured, so adding it would be an invention rather than a port. No [[deprecated]] either -- that is #2289's undecided question. Fixture set: 29 fixtures / 174 sites. Gate 17,280 run, 0 failed. Downstream: zero sites in either consumer.
1 parent 3892088 commit ea884d3

8 files changed

Lines changed: 277 additions & 27 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

audit/AUDIT_FINDINGS_INDEX.md

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<!-- SPDX-License-Identifier: MIT -->
2+
<!-- Copyright (c) Robert Vokac and contributors -->
3+
4+
# Migration — `MarshalByRefObject` is no longer directly constructible (ticket #2297)
5+
6+
*2026-08-18.* `System::MarshalByRefObject obj;` compiled. .NET declares the class `abstract` with
7+
a `protected` constructor, so C# rejects the equivalent with `CS0144`.
8+
9+
Landed under `docs/StandingApprovals.md` **SA-8** and **SA-9** with SA-2's five conditions.
10+
**Two of the ticket's three parts landed; the third needs a vtable approval** — see §3.
11+
12+
---
13+
14+
## 1. What changed
15+
16+
| Spelling | Was | Is |
17+
|---|---|---|
18+
| `System::MarshalByRefObject obj;` | compiled | **rejected** |
19+
| `new System::MarshalByRefObject()` | compiled | **rejected** |
20+
| `MarshalByRefObject sliced = derived;` | compiled | **rejected** |
21+
| `GetLifetimeService()` | **absent** | present, throws `PlatformNotSupportedException` |
22+
| `class X : public MarshalByRefObject {}` || **unchanged**, and the only intended use |
23+
| `AppDomain`, `ContextBoundObject` || **unchanged** |
24+
25+
`GetLifetimeService()`'s absence turned an **observable runtime diagnostic into a compile error at
26+
an unrelated place** — .NET keeps it precisely so a caller gets
27+
`PlatformNotSupportedException("Remoting is not supported on this platform.")`. It is **not**
28+
`virtual` in .NET, so adding it costs no vtable slot.
29+
30+
## 2. To migrate
31+
32+
Derive:
33+
34+
```cpp
35+
// before
36+
System::MarshalByRefObject obj;
37+
38+
// after
39+
class MyRemotable : public System::MarshalByRefObject {};
40+
MyRemotable obj;
41+
```
42+
43+
A reference or pointer to the base is unaffected.
44+
45+
## 3. What did NOT land, and why
46+
47+
**`InitializeLifetimeService()` is still absent.** .NET's is `virtual`, and this class already has
48+
a vtable (the destructor), so adding it inserts a **slot** — a vtable change here **and in both
49+
derived classes**, `AppDomain` and `ContextBoundObject`. `docs/StandingApprovals.md` SA-3 excludes
50+
vtable changes explicitly, so it needs its own approval: **ticket #2374**.
51+
52+
It is left absent rather than added **non-virtually**, because a non-virtual member of that name
53+
would silently defeat the one thing the .NET member exists for — letting a derived type override
54+
the lease policy. A test asserts its absence through a detection idiom, so #2374's arrival is
55+
visible rather than silent.
56+
57+
**`MemberwiseClone(bool)` is still absent.** .NET's calls `Object.MemberwiseClone()`, and
58+
`System::Object` in this port declares no such member — measured. Adding it would be an invention
59+
rather than a port, which SA-5 forbids.
60+
61+
**No `[[deprecated]]`.** Both .NET members carry `[Obsolete(RemotingApisMessage)]`. Whether .NET's
62+
`Obsolete` becomes C++ `[[deprecated]]` anywhere in this repository is the undecided ticket
63+
**#2289**, and answering it incidentally here would settle it in the wrong place.
64+
65+
## 4. Downstream, measured
66+
67+
Neither `cna` nor `mobile-eggbert` mentions `MarshalByRefObject` — **zero sites in both**. Neither
68+
repository was modified.

modules/core/include/System/MarshalByRefObject.hpp

Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,41 +3,81 @@
33
// Portions based on .NET runtime API (MIT License, Copyright .NET Foundation and Contributors)
44
#pragma once
55

6+
#include "System/PlatformNotSupportedException.hpp"
7+
68
namespace System {
79

810
/**
911
* @brief Enables access to objects across application domain boundaries in applications
1012
* that support remoting.
1113
*
12-
* C++ counterpart of .NET System.MarshalByRefObject.
13-
* In this port the class serves as a base-class marker; full remoting is not implemented.
14+
* C++ counterpart of .NET `System.MarshalByRefObject`
15+
* (`MarshalByRefObject.cs:10-33`), which is `public abstract` with a `protected`
16+
* constructor and whose two lifetime members exist only to throw
17+
* `PlatformNotSupportedException(SR.PlatformNotSupported_Remoting)`. Remoting is not
18+
* implemented in this port and never will be.
1419
*
15-
* @warning **This class's public shape is not .NET's, in two ways that are
16-
* observable rather than cosmetic** (SR-AUD-128; the shape decision is
17-
* ticket #2297 and neither divergence is repaired by this note).
20+
* @par What ticket #2297 changed
21+
* - **The constructor is `protected`**, matching .NET's, so `System::MarshalByRefObject obj;`
22+
* no longer compiles — C# rejects the equivalent with `CS0144`. The copy and move members
23+
* are protected with it, so the base cannot be reached by a slice either.
24+
* - **`GetLifetimeService()` is present and throws**, as .NET's does. Its absence turned an
25+
* observable runtime diagnostic into a compile error at an unrelated place.
1826
*
19-
* - **It is directly constructible.** `System::MarshalByRefObject obj;`
20-
* compiles here; .NET declares the class `abstract` with a `protected`
21-
* constructor, so the C# equivalent is rejected outright (`CS0144`). Two
22-
* in-repo tests construct the base today, so closing this is a public
23-
* source break, not a tidy-up.
24-
* - **Three .NET members are absent**, not merely unimplemented:
25-
* `GetLifetimeService()`, the virtual `InitializeLifetimeService()` and
26-
* the protected `MemberwiseClone(bool)`. Current .NET keeps the first two
27-
* specifically so that a caller receives `PlatformNotSupportedException`;
28-
* removing them replaces that observable diagnostic with a compile error
29-
* at an unrelated place. Adding them back is additive but introduces a
30-
* virtual, which changes this class's vtable and that of every derived
31-
* class — `AppDomain` and `ContextBoundObject` in this repository.
27+
* @par What ticket #2297 deliberately did NOT change, and why
28+
* - **`InitializeLifetimeService()` is still absent.** .NET's is `virtual`, and this class
29+
* already has a vtable (the destructor), so adding it inserts a **slot** — a vtable change
30+
* in this class and in both derived ones, `AppDomain` and `ContextBoundObject`.
31+
* `docs/StandingApprovals.md` SA-3 excludes vtable changes explicitly, so this needs its own
32+
* approval and is ticket **#2374**. It is left absent rather than added non-virtually,
33+
* because a non-virtual member of that name would silently break the one thing the .NET
34+
* member is for: letting a derived type override the lease policy.
35+
* - **`MemberwiseClone(bool)` is still absent.** .NET's calls `Object.MemberwiseClone()`, and
36+
* `System::Object` in this port declares no such member — measured. Adding it would be an
37+
* invention rather than a port, which SA-5 forbids.
38+
* - **No `[[deprecated]]`.** Both .NET members carry `[Obsolete(RemotingApisMessage)]`;
39+
* whether .NET's `Obsolete` becomes C++ `[[deprecated]]` anywhere in this repository is the
40+
* undecided ticket #2289, and answering it incidentally here would settle it in the wrong
41+
* place.
3242
*
33-
* What the port does provide is correct as far as it goes: the virtual
34-
* destructor makes polymorphic deletion through this base well defined,
35-
* which is what the derived types rely on.
43+
* The virtual destructor makes polymorphic deletion through this base well defined, which is
44+
* what the derived types rely on.
3645
*/
3746
class MarshalByRefObject {
47+
protected:
48+
/**
49+
* @brief Protected default constructor, matching .NET's `protected MarshalByRefObject()`.
50+
*
51+
* Ticket #2297. The copy and move members are protected with it so the base cannot be
52+
* reached by a slice.
53+
*/
54+
MarshalByRefObject() = default;
55+
MarshalByRefObject(const MarshalByRefObject&) = default;
56+
MarshalByRefObject(MarshalByRefObject&&) = default;
57+
MarshalByRefObject& operator=(const MarshalByRefObject&) = default;
58+
MarshalByRefObject& operator=(MarshalByRefObject&&) = default;
59+
3860
public:
3961
/** @brief Virtual destructor to allow safe polymorphic deletion. */
4062
virtual ~MarshalByRefObject() = default;
63+
64+
/**
65+
* @brief Always throws; remoting is not supported.
66+
*
67+
* C++ counterpart of .NET `MarshalByRefObject.GetLifetimeService()`
68+
* (`MarshalByRefObject.cs:17-21`), which throws
69+
* `PlatformNotSupportedException(SR.PlatformNotSupported_Remoting)`. It is **not**
70+
* `virtual` there, so adding it here costs no vtable slot.
71+
*
72+
* Added by ticket #2297: its absence turned an observable runtime diagnostic into a
73+
* compile error at an unrelated place.
74+
*
75+
* @return Never returns.
76+
* @throws System::PlatformNotSupportedException always.
77+
*/
78+
[[noreturn]] void* GetLifetimeService() const {
79+
throw PlatformNotSupportedException("Remoting is not supported on this platform.");
80+
}
4181
};
4282

4383
} // namespace System

modules/core/tests/System/Batch3TypeTests.cpp

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// MidpointRounding, UInt128, MarshalByRefObject, EventHandler,
66
// ReadOnlyMemory, Activator, ThreadStaticAttribute, FlagsAttribute
77
#include <gtest/gtest.h>
8+
#include "System/PlatformNotSupportedException.hpp"
89
#include <limits>
910
#include <type_traits>
1011
#include <vector>
@@ -86,8 +87,52 @@ TEST(MarshalByRefObjectNewTests, PolymorphicDeletion_NoLeak) {
8687
std::unique_ptr<System::MarshalByRefObject> p = std::make_unique<Derived>();
8788
EXPECT_NE(p.get(), nullptr);
8889
}
89-
TEST(MarshalByRefObjectNewTests, DefaultCtor_DoesNotThrow) {
90-
EXPECT_NO_THROW(System::MarshalByRefObject obj);
90+
namespace detail2297 {
91+
/// Detection idiom: true only if `T` has a callable `InitializeLifetimeService()`. Written as a
92+
/// concept over a dependent expression so that the member's ABSENCE is a value rather than a
93+
/// compile error -- which is what makes the assertion below expressible at all.
94+
template <typename T>
95+
concept HasInitializeLifetimeService = requires(T& t) { t.InitializeLifetimeService(); };
96+
} // namespace detail2297
97+
98+
TEST(MarshalByRefObjectNewTests, Fix2297_TheBaseIsNoLongerDirectlyConstructible) {
99+
// #2297 made the constructor protected, matching .NET's `protected MarshalByRefObject()`;
100+
// C# rejects the equivalent with CS0144. This test used to construct the base directly.
101+
struct Derived : System::MarshalByRefObject {};
102+
static_assert(!std::is_default_constructible_v<System::MarshalByRefObject>,
103+
"#2297: the constructor is protected");
104+
static_assert(!std::is_copy_constructible_v<System::MarshalByRefObject>,
105+
"#2297: and the copy member went with it, so the base cannot be sliced out");
106+
static_assert(std::is_default_constructible_v<Derived>,
107+
"a derived type is still constructible -- that is the point");
108+
EXPECT_NO_THROW(Derived obj; (void)obj);
109+
}
110+
111+
TEST(MarshalByRefObjectNewTests, Fix2297_GetLifetimeServiceThrowsAndTheVirtualOneIsStillAbsent) {
112+
// GetLifetimeService() is added: .NET keeps it precisely so a caller receives
113+
// PlatformNotSupportedException (`MarshalByRefObject.cs:17-21`), and its absence here turned
114+
// that observable diagnostic into a compile error at an unrelated place. It is NOT virtual in
115+
// .NET, so adding it costs no vtable slot.
116+
struct Derived : System::MarshalByRefObject {};
117+
Derived obj;
118+
EXPECT_THROW((void)obj.GetLifetimeService(), System::PlatformNotSupportedException);
119+
try {
120+
(void)obj.GetLifetimeService();
121+
ADD_FAILURE() << "expected PlatformNotSupportedException";
122+
} catch (const System::PlatformNotSupportedException& e) {
123+
EXPECT_STREQ(e.what(), "Remoting is not supported on this platform.");
124+
}
125+
126+
// InitializeLifetimeService() is DELIBERATELY still absent: .NET's is virtual, and this class
127+
// already has a vtable, so adding it inserts a SLOT -- a vtable change here and in both
128+
// derived classes. SA-3 excludes those explicitly. Ticket #2374 carries the approval request,
129+
// and this assertion is what makes its arrival visible rather than silent.
130+
// The `requires` form needs the name to be findable at all, so it is written as a
131+
// detection idiom over a dependent type instead -- an absent member must be expressible as
132+
// absent, not as a hard error.
133+
static_assert(!detail2297::HasInitializeLifetimeService<Derived>,
134+
"#2374 landed: InitializeLifetimeService() is present, so re-derive the vtable "
135+
"pins in AppDomain and ContextBoundObject");
91136
}
92137

93138
// ---------------------------------------------------------------------------

plan.sqlite3

4 KB
Binary file not shown.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// SPDX-License-Identifier: MIT
2+
// Copyright (c) Robert Vokac and contributors
3+
//
4+
// Negative compile fixture for ticket #2297 (SR-AUD-128).
5+
//
6+
// #2297 made System::MarshalByRefObject's constructor protected, matching
7+
// .NET's `protected MarshalByRefObject()` -- C# rejects the equivalent with
8+
// CS0144 -- and added GetLifetimeService(), which .NET keeps precisely so that
9+
// a caller receives PlatformNotSupportedException rather than a compile error
10+
// at an unrelated place.
11+
//
12+
// What it deliberately did NOT add is InitializeLifetimeService(). .NET's is
13+
// VIRTUAL, and this class already has a vtable (the destructor), so adding it
14+
// inserts a SLOT -- a vtable change here and in both derived classes, AppDomain
15+
// and ContextBoundObject. docs/StandingApprovals.md SA-3 excludes vtable
16+
// changes explicitly. Ticket #2374 carries that approval request.
17+
//
18+
// Records: docs/Migration-MarshalByRefObjectProtectedConstructor.md,
19+
// docs/NegativeConsumerFixtureValidation.md.
20+
//
21+
// NEGATIVE-FIXTURE: component=Core.Base
22+
#include <type_traits>
23+
24+
#include "System/MarshalByRefObject.hpp"
25+
#include "System/PlatformNotSupportedException.hpp"
26+
27+
#ifndef SHARP_RUNTIME_NEGATIVE_SITE
28+
#define SHARP_RUNTIME_NEGATIVE_SITE 0
29+
#endif
30+
31+
using System::MarshalByRefObject;
32+
33+
namespace {
34+
/// The migrated shape: derive, which is what a remotable type always had to do.
35+
class MyRemotable : public MarshalByRefObject {};
36+
} // namespace
37+
38+
int main() {
39+
#if SHARP_RUNTIME_NEGATIVE_SITE == 1
40+
// NEGATIVE(marshalbyrefobject-direct-instantiation): is protected within this context
41+
// | protected
42+
MarshalByRefObject direct;
43+
(void)direct;
44+
#else
45+
MyRemotable direct;
46+
(void)direct;
47+
#endif
48+
49+
#if SHARP_RUNTIME_NEGATIVE_SITE == 2
50+
// NEGATIVE(marshalbyrefobject-heap-instantiation): is protected within this context
51+
// | protected
52+
MarshalByRefObject* heap = new MarshalByRefObject();
53+
delete heap;
54+
#else
55+
MarshalByRefObject* heap = new MyRemotable();
56+
delete heap;
57+
#endif
58+
59+
#if SHARP_RUNTIME_NEGATIVE_SITE == 3
60+
// NEGATIVE(marshalbyrefobject-copy-slice): is protected within this context
61+
// | protected
62+
// | use of deleted function
63+
MyRemotable derived;
64+
MarshalByRefObject sliced = derived;
65+
(void)sliced;
66+
#else
67+
MyRemotable derived;
68+
const MarshalByRefObject& notSliced = derived;
69+
(void)notSliced;
70+
#endif
71+
72+
#if SHARP_RUNTIME_NEGATIVE_SITE == 4
73+
// NEGATIVE(marshalbyrefobject-still-default-constructible): static assertion failed
74+
// | static_assert
75+
// The shape that breaks a consumer SILENTLY: a trait query.
76+
static_assert(std::is_default_constructible_v<MarshalByRefObject>,
77+
"MarshalByRefObject is expected to be default-constructible");
78+
#else
79+
static_assert(!std::is_default_constructible_v<MarshalByRefObject>,
80+
"#2297: the constructor is protected, as .NET's is");
81+
static_assert(std::is_default_constructible_v<MyRemotable>,
82+
"#2297: a derived type is still constructible -- that is the point");
83+
#endif
84+
85+
// UNCHANGED / ADDED, and asserted so the fixture proves what the ticket DID provide:
86+
// GetLifetimeService() exists and is reachable from a derived type. It is not virtual in
87+
// .NET, so adding it cost no vtable slot.
88+
try {
89+
(void)derived.GetLifetimeService();
90+
return 1; // must not be reached
91+
} catch (const System::PlatformNotSupportedException&) {
92+
return 0;
93+
}
94+
}

tests/integration/Task42Tests.cpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1459,8 +1459,11 @@ TEST(PredicateTests, Predicate_IsEven) {
14591459
// MarshalByRefObject
14601460
// ===========================================================================
14611461

1462-
TEST(MarshalByRefObjectTests, Instantiation_NoThrow) {
1463-
EXPECT_NO_THROW(System::MarshalByRefObject obj);
1462+
TEST(MarshalByRefObjectTests, Fix2297_DerivedInstantiation_NoThrow) {
1463+
// #2297 made the base's constructor protected, matching .NET. Deriving is the only route,
1464+
// and the only one there ever should have been.
1465+
struct Derived : System::MarshalByRefObject {};
1466+
EXPECT_NO_THROW(Derived obj; (void)obj);
14641467
}
14651468

14661469
// ===========================================================================

0 commit comments

Comments
 (0)