Skip to content

Commit 01b9691

Browse files
committed
Merge branch 'rework/strip-rtti'
2 parents 9f1ffc1 + 76261c8 commit 01b9691

4 files changed

Lines changed: 79 additions & 37 deletions

File tree

compiler/ncgrtti.pas

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,11 @@ interface
3131
symbase,symconst,symtype,symdef,symsym,
3232
parabase;
3333

34-
{ returns s if rtti stripping is off or def is whitelisted via df_expose_rtti,
35-
otherwise returns empty string. used by ncgrtti/ncgvmt at every place where
36-
a type-name shortstring would otherwise leak into the binary }
37-
function rtti_string(const s: shortstring; def: tdef = nil): shortstring;
34+
{ returns s if rtti stripping is off or def/parent_def is whitelisted via
35+
df_expose_rtti, otherwise returns empty string. parent_def is for member
36+
strings (field/method/property/param/enum value): if the owning type is
37+
exposed, the member keeps its name too }
38+
function rtti_string(const s: shortstring; def: tdef = nil; parent_def: tdef = nil): shortstring;
3839

3940
type
4041

@@ -74,7 +75,7 @@ TRTTIWriter=class
7475
procedure write_callconv(tcb:ttai_typedconstbuilder;def:tabstractprocdef);
7576
procedure write_paralocs(tcb:ttai_typedconstbuilder;para:pcgpara);
7677
procedure write_param_flag(tcb:ttai_typedconstbuilder;parasym:tparavarsym);
77-
procedure write_param(tcb:ttai_typedconstbuilder;para:tparavarsym);
78+
procedure write_param(tcb:ttai_typedconstbuilder;para:tparavarsym;parent_def:tdef=nil);
7879
procedure write_mop_offset_table(tcb:ttai_typedconstbuilder;def:tabstractrecorddef;mop:tmanagementoperator);
7980
procedure maybe_add_comment(tcb:ttai_typedconstbuilder;const comment : string); inline;
8081
public
@@ -126,11 +127,12 @@ TPropNameListItem = class(TFPHashObject)
126127
end;
127128

128129

129-
function rtti_string(const s: shortstring; def: tdef = nil): shortstring;
130+
function rtti_string(const s: shortstring; def: tdef = nil; parent_def: tdef = nil): shortstring;
130131
begin
131132
result := s;
132133
if not (m_strip_rtti in current_settings.modeswitches) then exit;
133134
if assigned(def) and (df_expose_rtti in tstoreddef(def).defoptions) then exit;
135+
if assigned(parent_def) and (df_expose_rtti in tstoreddef(parent_def).defoptions) then exit;
134136
result := '';
135137
end;
136138

@@ -328,7 +330,7 @@ TPropNameListItem = class(TFPHashObject)
328330
else
329331
tcb.emit_procdef_const(def.invoke_helper);
330332
maybe_add_comment(tcb,#9'name');
331-
tcb.emit_pooled_shortstring_const_ref(sym.realname);
333+
tcb.emit_pooled_shortstring_const_ref(rtti_string(sym.realname,nil,tdef(st.defowner)));
332334

333335
if extended_rtti then
334336
begin
@@ -348,7 +350,7 @@ TPropNameListItem = class(TFPHashObject)
348350
end;
349351

350352
for k:=0 to def.paras.count-1 do
351-
write_param(tcb,tparavarsym(def.paras[k]));
353+
write_param(tcb,tparavarsym(def.paras[k]),tdef(st.defowner));
352354

353355
if not is_void(def.returndef) then
354356
begin
@@ -554,7 +556,7 @@ TPropNameListItem = class(TFPHashObject)
554556
end;
555557

556558
procedure TRTTIWriter.write_param(tcb: ttai_typedconstbuilder;
557-
para: tparavarsym);
559+
para: tparavarsym; parent_def: tdef);
558560
begin
559561
maybe_add_comment(tcb,'RTTI: begin param '+para.prettyname);
560562
tcb.begin_anonymous_record('',defaultpacking,min(reqalign,SizeOf(PInt)),
@@ -571,7 +573,7 @@ TPropNameListItem = class(TFPHashObject)
571573
write_param_flag(tcb,para);
572574

573575
maybe_add_comment(tcb,#9'name');
574-
tcb.emit_pooled_shortstring_const_ref(para.realname);
576+
tcb.emit_pooled_shortstring_const_ref(rtti_string(para.realname,nil,parent_def));
575577

576578
maybe_add_comment(tcb,#9'locs');
577579
write_paralocs(tcb,@para.paraloc[callerside]);
@@ -891,7 +893,7 @@ TExtendedFieldInfo = record
891893
{ FieldVisibility }
892894
tcb.emit_ord_const(visibility_to_rtti_flags(fldsym.visibility),u8inttype);
893895
{ Name }
894-
tcb.emit_pooled_shortstring_const_ref(fldsym.realname);
896+
tcb.emit_pooled_shortstring_const_ref(rtti_string(fldsym.realname,nil,def));
895897
{ Attribute table }
896898
if assigned(fldsym.rtti_attribute_list) and assigned(fldsym.rtti_attribute_list.rtti_attributes) then
897899
cnt:=fldsym.rtti_attribute_list.rtti_attributes.count
@@ -1112,7 +1114,7 @@ ((def.typ=recorddef) or
11121114
begin
11131115
if tsym(paramst.symlist[i]).typ<>paravarsym then
11141116
Internalerror(2024103101);
1115-
write_param(paramtcb,tparavarsym(paramst.symlist[i]));
1117+
write_param(paramtcb,tparavarsym(paramst.symlist[i]),tdef(st.defowner));
11161118
end;
11171119

11181120

@@ -1198,7 +1200,7 @@ ((def.typ=recorddef) or
11981200
{ write property name }
11991201
if addcomments then
12001202
tcb.emit_comment(#9'name');
1201-
tcb.emit_shortstring_const(rtti_string(sym.realname));
1203+
tcb.emit_shortstring_const(rtti_string(sym.realname,nil,tdef(st.defowner)));
12021204
result:=tcb.end_anonymous_record;
12031205
if addcomments then
12041206
tcb.emit_comment('RTTI: End propinfo record '+sym.realname);
@@ -1372,7 +1374,7 @@ TPropInfoEx = record
13721374
if hp.value>def.maxval then
13731375
break;
13741376
tcb.next_field_name:=hp.name;
1375-
tcb.emit_shortstring_const(rtti_string(hp.realname));
1377+
tcb.emit_shortstring_const(rtti_string(hp.realname,nil,def));
13761378
end;
13771379
{ write unit name }
13781380
tcb.emit_shortstring_const(rtti_string(current_module.realmodulename^));
@@ -1826,7 +1828,7 @@ TPropInfoEx = record
18261828
{ write flags for current parameter }
18271829
write_param_flag(tcb,parasym);
18281830
{ write name of current parameter }
1829-
tcb.emit_shortstring_const(rtti_string(parasym.realname));
1831+
tcb.emit_shortstring_const(rtti_string(parasym.realname,nil,def));
18301832
{ write name of type of current parameter }
18311833
write_rtti_name(tcb,parasym.vardef);
18321834
end;
@@ -1848,7 +1850,7 @@ TPropInfoEx = record
18481850
else
18491851
write_rtti_reference(tcb,parasym.vardef,fullrtti);
18501852
{ write name of current parameter }
1851-
tcb.emit_shortstring_const(rtti_string(parasym.realname));
1853+
tcb.emit_shortstring_const(rtti_string(parasym.realname,nil,def));
18521854
tcb.end_anonymous_record;
18531855
end;
18541856

compiler/ncgvmt.pas

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ tvmtasmoutput = record
494494
begin
495495
{ l: name_of_method }
496496
lists^.pubmethodstcb.start_internal_data_builder(current_asmdata.AsmLists[al_const],sec_rodata,_class.vmt_mangledname,datatcb,l);
497-
namedef:=datatcb.emit_shortstring_const(tsym(p).realname);
497+
namedef:=datatcb.emit_shortstring_const(rtti_string(tsym(p).realname,nil,_class));
498498
lists^.pubmethodstcb.finish_internal_data_builder(datatcb,l,namedef,sizeof(pint));
499499
{ the tmethodnamerec }
500500
lists^.pubmethodstcb.maybe_begin_aggregate(lists^.methodnamerec);
@@ -701,7 +701,7 @@ classindex:=classtablelist.IndexOf(tfieldvarsym(sym).var
701701
if classindex=-1 then
702702
internalerror(200611033);
703703
datatcb.emit_tai(Tai_const.Create_16bit(classindex+1),u16inttype);
704-
datatcb.emit_shortstring_const(tfieldvarsym(sym).realname);
704+
datatcb.emit_shortstring_const(rtti_string(tfieldvarsym(sym).realname,nil,_class));
705705
datatcb.end_anonymous_record;
706706
end;
707707
end;

compiler/ngenutil.pas

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ implementation
167167
aasmtai,aasmcnst,
168168
symbase,symtable,defutil,
169169
nadd,ncal,ncnv,ncon,nflw,ninl,nld,nmem,nutils,
170+
ncgrtti,
170171
ppu,
171172
pass_1,
172173
export;
@@ -1298,7 +1299,7 @@ implementation
12981299
begin
12991300
{ Create string constant and emit pointer to it }
13001301
unitinits.start_internal_data_builder(current_asmdata.asmlists[al_globals],sec_rodata,'',unitnametcb,unitnamelbl);
1301-
unitnamedef:=unitnametcb.emit_shortstring_const(entry^.module.realmodulename^);
1302+
unitnamedef:=unitnametcb.emit_shortstring_const(rtti_string(entry^.module.realmodulename^));
13021303
unitinits.finish_internal_data_builder(unitnametcb,unitnamelbl,unitnamedef,sizeof(pint));
13031304
unitinits.queue_init(charpointertype);
13041305
unitinits.queue_emit_asmsym(unitnamelbl,unitnamedef);

unleashed/docs/strip-rtti.md

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,32 @@ Strip RTTI does not remove the RTTI structures themselves (they are still walked
2121

2222
When `striprtti` is active, the following shortstrings are nulled:
2323

24-
| Source | What it is | Stripped |
24+
| Source | What it is | Whitelist via `expose` |
2525
|---|---|---|
26-
| RTTI header (`write_header`) | Type name in the `TTypeInfo` block | yes |
27-
| Object/class RTTI (`write_objectdef_rtti`) | Class real name (`def.objrealname`) | yes |
28-
| Class VMT (`ncgvmt`) | Class name in the VMT (`_class.RttiName`) | yes |
29-
| Member RTTI (`write_member_rtti`) | Field/property/method name (`sym.realname`) | yes |
30-
| Used-units list | Module name in the units-of-use list (`hp.realname`) | yes |
31-
| Module name (multiple sites) | `current_module.realmodulename^` written into class/interface/object RTTI | yes |
32-
| Parameter names | Method parameter names in published method RTTI | yes |
33-
| Type alias name | The `prettyname` written by `write_rtti_data_singleref` for an alias | yes |
26+
| RTTI header (`write_header`) | Type name in the `TTypeInfo` block | on the type itself |
27+
| Object/class RTTI (`write_objectdef_rtti`) | Class real name (`def.objrealname`) | on the class |
28+
| Class VMT (`ncgvmt`) | Class name in the VMT (`_class.RttiName`) | on the class |
29+
| Type alias name | The `prettyname` written by `write_rtti_data_singleref` | on the alias |
30+
| Property name (typeinfo) | `sym.realname` for published property | on the owning class |
31+
| Enum value names | `hp.realname` for each enum value | on the enum type |
32+
| Procvar parameter names | `parasym.realname` in procvar RTTI | on the procvar type |
33+
| Method parameter names (extended RTTI) | `para.realname` in extended method table | on the owning class |
34+
| Published method name (VMT method table) | `tsym.realname` used by `MethodAddress` | on the owning class |
35+
| Published method name (extended RTTI) | `sym.realname` in extended method entry | on the owning class |
36+
| Published field name (VMT field table) | `tfieldvarsym.realname` used by `FieldAddress` | on the owning class |
37+
| Published field name (extended RTTI) | `fldsym.realname` in extended field entry | on the owning class |
38+
| Module name (RTTI structs) | `current_module.realmodulename^` in class/interface/object RTTI | (not whitelistable per type) |
39+
| Module name (unit init/finalize table) | Used-unit names in the init/fini dispatch table | (not whitelistable per type) |
40+
| Used-units list | Module name in the units-of-use list (`hp.realname`) | (not whitelistable per type) |
41+
42+
**Whitelist propagation:** the `expose` keyword on a type sets `df_expose_rtti` on its `tdef`. Members of an exposed type (its properties, enum values, procvar parameters, published methods/fields, and method parameters) inherit the whitelist - their names stay in the binary too. Without `expose` on the parent, members are stripped even if you want a single property name visible; this is by design (whitelist is a per-type opt-in).
3443

3544
The following are **not** stripped (intentionally):
3645

3746
| Source | Why |
3847
|---|---|
3948
| Interface GUID string (`def.iidstr^` for `odt_interfacecorba`) | Functional - COM dispatch and `IUnknown.QueryInterface` look it up by string. Stripping breaks COM. |
49+
| String message handler names (`procedure foo; message 'bar';`) | Functional - runtime message dispatch (Cocoa, Symbian) uses the string for lookup. |
4050
| Format strings, `writeln` arguments, RTL string constants | These are not RTTI - they are program data. |
4151
| Symbol names exposed to the linker | Linker-visible symbols are governed by smart-linking and `{$L+}`, not by RTTI stripping. |
4252

@@ -68,6 +78,32 @@ type
6878
expose TColor = (red, green, blue); // and on enums, sets, ranges, aliases...
6979
```
7080

81+
#### Where `expose` can be used
82+
83+
The keyword is a generic prefix: parser sets a boolean before reading the type, and the resulting `tdef` (whatever kind) gets `df_expose_rtti`. So `expose` works in front of every kind of type Pascal allows in a `type` block:
84+
85+
| Type kind | Example |
86+
|---|---|
87+
| class | `expose TForm1 = class(TForm) ... end;` |
88+
| object | `expose TOldObj = object ... end;` |
89+
| interface | `expose IFoo = interface ... end;` |
90+
| record | `expose TPoint = record x, y: integer; end;` |
91+
| class helper | `expose TStrHelper = class helper for string ... end;` |
92+
| record / type helper | `expose THelp = record helper for integer ... end;` |
93+
| enumeration | `expose TColor = (red, green, blue);` |
94+
| subrange | `expose TDay = 1..7;` |
95+
| set | `expose TColors = set of TColor;` |
96+
| static array | `expose TBuf = array[0..15] of byte;` |
97+
| dynamic array | `expose TIntArr = array of integer;` |
98+
| pointer | `expose PNode = ^TNode;` |
99+
| procedural / procvar | `expose TCallback = procedure(x: integer) of object;` |
100+
| weak alias | `expose TMyInt = integer;` |
101+
| strong alias | `expose TMyInt = type integer;` |
102+
| generic | `expose generic TList<T> = class ... end;` |
103+
| file type | `expose TLogFile = file of TRecord;` |
104+
105+
What gets kept depends on the type kind, because of the propagation rules - exposing a class keeps its property/method/field/method-param names; exposing an enum keeps its value names; exposing a procvar keeps its parameter names; exposing a record keeps the type name. See the propagation rules above.
106+
71107
The keyword is gated on `m_unleashed`, not on `m_strip_rtti`. That means:
72108

73109
- In any other mode, `expose` is a regular identifier - existing code with a field, variable, or routine called `expose` keeps compiling.
@@ -134,17 +170,17 @@ Comparisons are case-insensitive (patterns are lowercased on insertion, names lo
134170

135171
## Side effects
136172

137-
Compiling LCL or anything that walks RTTI by string with `striprtti` on will likely brick the program at startup, because code such as:
138-
139-
```pas
140-
Application.CreateForm(TForm1, Form1);
141-
```
173+
Anything that walks RTTI by string and isn't whitelisted will fail at runtime. Concrete cases:
142174

143-
resolves `TForm1` against the resource section by **string** comparison. With `striprtti` and no whitelist, the compiler emits `''` for the type name - so the lookup looks for `""` and fails.
175+
- **`Application.CreateForm(TForm1, Form1)`** - resolves `TForm1` against the resource section by string comparison. With `striprtti` and no whitelist, the type name is `''` and the lookup fails.
176+
- **`SomeObject.MethodAddress('OnClick')`** - the VMT method table has empty names for stripped methods, so the lookup returns `nil`. LCL component event hookup uses this path during form streaming.
177+
- **`SomeObject.FieldAddress('myButton')`** - same story for the VMT field table.
178+
- **`GetPropInfo(SomeObject, 'Caption')`** - empty property names in extended RTTI, lookup fails.
179+
- **`WriteStr(s, someEnumValue)` / `ReadStr(s, someEnumValue)`** - empty enum value names produce empty output / fail to parse input.
144180

145181
The fix is one of:
146182

147-
- whitelist the affected types (preferred): `--rttiexpose=TForm*,TFrame*,TDataModule*` or `expose TForm1 = class(...)` per declaration,
183+
- whitelist the affected types: `--rttiexpose=TForm*,TFrame*,TDataModule*` or `expose TForm1 = class(...)` per declaration,
148184
- enable `striprtti` only in units that do not need RTTI lookup (e.g. business-logic units, but not units containing forms),
149185
- leave `striprtti` off for the whole project (default).
150186

@@ -184,8 +220,11 @@ end.
184220

185221
## Implementation notes
186222

187-
- Decision is encoded as `df_expose_rtti` on `tdef.defoptions` (set during parsing). RTTI emit reads it via the helper `rtti_string(s, def)` in `ncgrtti`.
188-
- `rtti_string` returns `s` if `striprtti` is off **or** `df_expose_rtti` is set on `def`; otherwise returns `''`. Sites that emit a name without an associated `tdef` (module name, parameter name) call `rtti_string(s)` without `def` - they cannot be whitelisted by name.
223+
- Decision is encoded as `df_expose_rtti` on `tdef.defoptions` (set during parsing). RTTI emit reads it via the helper `rtti_string(s, def, parent_def)` in `ncgrtti`.
224+
- `rtti_string` returns `s` if `striprtti` is off **or** `df_expose_rtti` is set on `def` **or** on `parent_def`; otherwise returns `''`.
225+
- `def` is the type whose name is being emitted (used at type-name emit sites).
226+
- `parent_def` is the owning type for member strings: the class for a property/method/field name, the enum for an enum value, the procvar for a parameter name. This is how an `expose` on the parent propagates to its members.
227+
- Sites that emit a name without any associated `tdef` (module name in unit init/finalize table) pass neither - those are stripped unconditionally and not whitelistable per type.
189228
- The flag is preserved across PPU - whitelisting decisions made in one compile run survive into binary form, so dependent units see the same `df_expose_rtti` state without re-running `--rttiexpose=` matching.
190229
- Forward declarations (`type TFoo = class;`) - `expose` on the forward applies the flag to the same `tdef` that the final declaration completes, so both writes see it. `{$rttiexpose}` and `--rttiexpose=` match the name when the final declaration is parsed.
191230
- Generic specialization - the flag follows the specialized def. If you `expose TList<T> = class ...`, every specialization (`TList<integer>`, `TList<string>`, etc.) inherits the flag.

0 commit comments

Comments
 (0)