Skip to content

Commit bdcdb81

Browse files
committed
winmd-inspect: close over signature-named value types and delegates
Extend the `--closure` walk to pull in the value-type and delegate types a rendered interface names through its method signatures — the E3 value arm, the E6 struct-field types, and the E7 delegate signatures — so the closure emits everything a consumer needs to compile the projection, not the interface surface alone. Each projected type is an ABI-faithful shape, the way the interface projects an `@com` protocol that lowers to the COM vtable. `emit` now dispatches on the row's kind (`found[4]`): a struct through `structure`, an enum through `enumeration`, a delegate through `delegation`, and an interface through `interface` (the former inline body, now its own half), each rendered under exactly one of the template's `{{#struct}}`/`{{#enum}}`/ `{{#delegate}}`/`{{#interface}}` sections. The `walk` seeds and the `requires` bases carry the kind at index 4 so the walk and emit dispatch on the same column a `references` row already has. The projected shapes match how the target language's import models them. A struct is an `@frozen` struct, its guaranteed layout the C/C++ record it mirrors field for field, with a public memberwise initialiser (the synthesised one is internal, so a cross-module caller could not otherwise construct it). A field literally named `self` — escaped `` `self` `` for its stored property — takes a distinct collision-free local as the initialiser parameter's internal name and the assignment right-hand side (`` init(`self` arg0:) { self.`self` = arg0 } ``); reusing the field spelling would bind the assignment's leading `self` to the parameter, not the struct instance, and fail to compile. A field named `_` — the wildcard, no name — is added to the language spec's reserved words so `SANITIZE` escapes it to `` `_` `` for a declarable property; unlike `self` it does not shadow the instance, so it needs no distinct local. Only instance fields are storage: a static or literal field (the `fdStatic` bit) is dropped, and the dependency walk applies the same filter so a type named only by a dropped field is not pulled in. A value type whose ABI layout `@frozen` cannot reproduce — an explicit layout, a non-default packing, or a declared class size — is rejected as a frontier rather than misprojected; the `layout` query returns a row only for such a type (`ClassLayout` is an optional table the adapter now synthesises empty when absent). An enum projects to an explicitly-stored raw-value struct newtype, because a native `enum E: <underlying>` does not carry its raw type's ABI width; a `[flags]` enum (a `System.FlagsAttribute`, detected through the overridable `flags` query) is the same newtype refined to `OptionSet`, typing `rawValue` as the underlying type directly so the OptionSet `RawValue` associated type is inferred rather than spelled as an explicit `typealias` — a member legitimately named `RawValue` then stays a `static var` and does not collide with a support typealias. The `value__` storage field is found by its raw metadata name, so a `SANITIZE` override respelling it does not drop the enum to the `i4` fallback width, and each member's value is the constant `value(field:)` already formats per signedness. A delegate is an `@com(interface:)` protocol carrying a single `Invoke`, decoding its runtime IID through the `guid` query. Signature adjacency (E3/E6/E7) runs per kind — an interface over its methods, a struct over its instance fields, a delegate over its `Invoke` — resolving each referenced type through `references` by the exact row it was named through, never by (namespace, name), so two same-named nested types are not conflated. A resolved runtime `class` stays a frontier; an external `TypeRef`-only reference resolves to nothing and drops; an interface or delegate bearing no `GuidAttribute` — whose `@com` shape would spell a GUID-less `""` — is dropped. A reached type the language import already provides — a `Dialect.known` identity such as `HRESULT` — is a frontier the closure emits no wrapper for, since the consumer has it from the import. A signature-named value type or delegate emits at top level by its bare name. Metadata nesting (`Outer.Inner`) and the namespace disambiguation of same-named value types are follow-ups; this change lands the per-kind projections and the signature closure on the unique-name path. Integration tests over hand-built fixtures cover a struct-returning method (the `@frozen` struct with its public initialiser, emitted before the interface, a field named `self` given a distinct initialiser local, and a field named `_` escaped to `` `_` ``), an enum parameter (a stored newtype, a duplicate raw value as two constants, a `[flags]` `OptionSet`, and a flags member named `RawValue` kept distinct from the inferred support type), a delegate parameter (its `@com` protocol and decoded IID), and a signature-named interface enqueued and emitted; further tests pin an auto/explicit/packed/sized struct and a static struct field each dropped, a GUID-less interface and a language-provided type each frontiered, and a unique-named value type emitted bare with no namespace. The `fields` render query gains `Flags` and raw-name columns so the walk and the struct render drop a static field and the enum render finds its `value__` storage. Render-query shadowing is a supported extension point, though, so a `-I` override carrying the former two-column shape (`Id`, `Name`) is tolerated rather than trapping on the absent columns: a field with no `Flags` counts as instance storage, as it did before the columns existed, and the raw-name read falls back to the escaped `Name`. The `requires` walk tolerates a copied override of the same kind. The bundled query returns a trailing `kind` column ('interface') the walk and emit read from `found[4]`; an override copied from the earlier four-column shape (`Id`, namespace, name, `iid`) omits it. A base row shorter than five columns is padded to the interface shape before recursion — an `InterfaceImpl` base is always an interface — so the closure resolves and emits it rather than trapping on the absent column. The closure is scoped to pure COM: the walk frontiers a parameterised (generic) type — its `TypeName` bears an arity suffix — so a WinRT parameterised interface or delegate is never emitted, only referenced as a consumer-supplied type. A delegate projects one `@com` protocol (no generic wrapper/ABI-protocol arm), and `delegation` builds no generic-parameter clause.
1 parent ded5535 commit bdcdb81

12 files changed

Lines changed: 1254 additions & 42 deletions

File tree

Sources/SQLEngineWinMD/Database+SQL.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ extension WinMD.Storage: SQLEngine.Catalog {
6565
/// physically-present tables plus the referenced optionals.
6666
private static var optionals: Array<(name: String, schema: TableSchema.Type)> {
6767
[(name: "TypeSpec", schema: Metadata.Tables.TypeSpec.self),
68-
(name: "NestedClass", schema: Metadata.Tables.NestedClass.self)]
68+
(name: "NestedClass", schema: Metadata.Tables.NestedClass.self),
69+
(name: "ClassLayout", schema: Metadata.Tables.ClassLayout.self)]
6970
}
7071

7172
/// The relation named `name`, resolved case-insensitively against the

Sources/winmd-inspect/Resources/Languages/swift.lang

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ guid-clsid CLSID
5050
# The well-known projection of resolved CLR types to `COM` module symbols.
5151
wellknown Windows.Win32.Foundation.HRESULT HRESULT
5252
wellknown Windows.Win32.Foundation.BOOL BOOL
53+
# `_` is the wildcard pattern, not a name: a metadata identifier spelled `_`
54+
# must be backtick-escaped (`` `_` ``) to project as a usable declaration.
55+
keyword _
5356
keyword Any
5457
keyword as
5558
keyword associatedtype
Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
-- A struct's fields, in declaration order, through the `fields` view bound by
22
-- the struct's `Id` — each field's `Id` (for the render to decode its type from
3-
-- its `FieldDef` signature) and its keyword-escaped `Name`. The render spells
4-
-- each field's type at render time from the target `Dialect`, so the query
5-
-- projects only the identity and name, the way `params` does for a method.
3+
-- its `FieldDef` signature), its keyword-escaped `Name`, its `Flags`, and its
4+
-- raw (unsanitized) metadata name. The render spells each field's type
5+
-- at render time from the target `Dialect`, so the query projects the identity
6+
-- and name the way `params` does for a method; the `Flags` let the struct
7+
-- render drop a static or literal field (the `fdStatic` bit), which is not
8+
-- instance storage, while the enum render (which reads the same view for its
9+
-- members) keeps them. The raw name lets the enum render find its `value__`
10+
-- storage field by its metadata name, unaffected by a `SANITIZE` override that
11+
-- would change the escaped spelling the way a member's does.
612
SELECT
7-
Id,
8-
SANITIZE(Name) AS Name
13+
f.Id,
14+
SANITIZE(f.Name) AS Name,
15+
d.Flags AS Flags,
16+
f.Name AS Raw
917
FROM
10-
fields
18+
fields f
19+
JOIN FieldDef d ON d.Id = f.Id
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
-- Whether the enum `TypeDef` at `:parent` bears a `System.FlagsAttribute`
2+
-- custom attribute — the `[flags]` marking that makes the enum a bitmask. It
3+
-- returns the enum's `Id` when the attribute is present and no row otherwise,
4+
-- so the render projects a `[flags]` enum as an `OptionSet` (its members
5+
-- combine) and a plain enum as a native fixed-size Swift `enum`.
6+
--
7+
-- The attribute is matched by its (namespace, name) the way `guid.sql` and the
8+
-- `interfaces` view match `GuidAttribute` — through the three encodings a
9+
-- `CustomAttribute` names its constructor by: a `MemberRef` to a `TypeRef` (an
10+
-- external reference, the shape real metadata uses for `System.FlagsAttribute`),
11+
-- a `MethodDef` on the attribute `TypeDef` (a local definition), and a
12+
-- `MemberRef` to a `TypeDef` (a `MemberRef` naming a local definition). Only the
13+
-- namespace and name differ from `guid.sql`: `System.FlagsAttribute`, not the
14+
-- Win32 metadata `GuidAttribute`.
15+
--
16+
-- `CustomAttribute`/`MemberRef` are present in real metadata; a fixture that
17+
-- omits the matching rows simply yields no row, so the enum reads as not a
18+
-- `[flags]` enum — the same graceful non-match the `guid` query relies on.
19+
SELECT
20+
t.Id AS flags
21+
FROM
22+
TypeDef t
23+
JOIN CustomAttribute c ON c.Parent_TypeDef = t.Id
24+
JOIN MemberRef r ON c.Type_MemberRef = r.Id
25+
JOIN TypeRef g ON r.Class_TypeRef = g.Id
26+
WHERE
27+
g.TypeNamespace = 'System'
28+
AND g.TypeName = 'FlagsAttribute'
29+
AND t.Id = :parent
30+
UNION
31+
SELECT
32+
t.Id AS flags
33+
FROM
34+
TypeDef t
35+
JOIN CustomAttribute c ON c.Parent_TypeDef = t.Id
36+
JOIN MethodDef m ON c.Type_MethodDef = m.Id
37+
JOIN TypeDef g ON m.TypeDef = g.Id
38+
WHERE
39+
g.TypeNamespace = 'System'
40+
AND g.TypeName = 'FlagsAttribute'
41+
AND t.Id = :parent
42+
UNION
43+
SELECT
44+
t.Id AS flags
45+
FROM
46+
TypeDef t
47+
JOIN CustomAttribute c ON c.Parent_TypeDef = t.Id
48+
JOIN MemberRef r ON c.Type_MemberRef = r.Id
49+
JOIN TypeDef g ON r.Class_TypeDef = g.Id
50+
WHERE
51+
g.TypeNamespace = 'System'
52+
AND g.TypeName = 'FlagsAttribute'
53+
AND t.Id = :parent
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
-- Whether the value type at `:parent` has an ABI layout the `@frozen` struct
2+
-- projection cannot faithfully reproduce: it returns the type's `Id` when the
3+
-- type must be rejected, no row when it renders safely.
4+
--
5+
-- Only a sequential layout is safe. The `0x18` LayoutMask (§II.23.1.15) marks a
6+
-- value type auto (`0x00`), sequential (`0x08` tdSequentialLayout), or explicit
7+
-- (`0x10` tdExplicitLayout). An auto layout lets the runtime lay the fields out
8+
-- in whatever order it chooses, so their declaration (FieldDef) order need not
9+
-- be their memory order — a Swift `struct`, which lays its stored properties out
10+
-- in source order, would then mismatch. An explicit layout places fields at
11+
-- declared offsets — an overlapping union or a hand-laid record — which a Swift
12+
-- `struct` cannot express. Only a sequential layout guarantees the fields lie in
13+
-- declaration order at their natural offsets, the one arrangement a `struct`
14+
-- reproduces, so the check admits `0x08` alone and rejects auto, explicit, and
15+
-- the reserved `0x18`.
16+
--
17+
-- A non-default packing (a `ClassLayout` row whose `PackingSize` is not zero)
18+
-- tightens the fields below their natural alignment, shifting every offset past
19+
-- the first. A declared size (a `ClassLayout` row whose `ClassSize` is not zero,
20+
-- §II.22.8) fixes the total in-memory extent — padding the record out to a
21+
-- larger size, or forcing tail padding a naturally-laid struct would not carry —
22+
-- which no `@frozen` `struct` reproduces either. `@frozen` only freezes the
23+
-- layout Swift itself chooses; it applies neither an explicit offset, a packing,
24+
-- nor a declared size, so such a type would decode to the wrong size and field
25+
-- offsets when passed to a native API. The closure walk rejects a type this
26+
-- query returns a row for, leaving it a frontier the consumer defines rather
27+
-- than an ABI-incompatible declaration. A `ClassSize` of zero is unspecified
28+
-- (the natural size stands), so it is safe; any non-zero declared size is
29+
-- rejected conservatively.
30+
--
31+
-- `ClassLayout` is an optional table (`table(named:)` resolves it to an empty
32+
-- relation when absent), so a database with no laid-out type reads no packing or
33+
-- size row and the LEFT JOIN yields NULL, which `COALESCE` treats as the default.
34+
SELECT
35+
t.Id
36+
FROM
37+
TypeDef t
38+
LEFT JOIN ClassLayout c ON c.Parent = t.Id
39+
WHERE
40+
t.Id = :parent
41+
AND (BITAND(t.Flags, 24) <> 8
42+
OR COALESCE(c.PackingSize, 0) <> 0
43+
OR COALESCE(c.ClassSize, 0) <> 0)

Sources/winmd-inspect/Resources/Render/requires.sql

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ SELECT
5454
n.Id,
5555
n.TypeNamespace,
5656
n.TypeName,
57-
n.iid
57+
n.iid,
58+
'interface' AS kind
5859
FROM
5960
InterfaceImpl i
6061
JOIN resolved rv ON rv.ref = i.Interface_TypeRef
@@ -66,7 +67,8 @@ SELECT
6667
n.Id,
6768
n.TypeNamespace,
6869
n.TypeName,
69-
n.iid
70+
n.iid,
71+
'interface' AS kind
7072
FROM
7173
InterfaceImpl i
7274
JOIN interfaces n ON n.Id = i.Interface_TypeDef

Sources/winmd-inspect/Resources/Templates/com.mustache

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,69 @@ public protocol {{{name}}}{{#base}}: {{{.}}}{{/base}} {
77
{{/methods}}
88
}
99
{{/interface}}
10+
{{#struct}}
11+
// @frozen fixes the ABI layout, so the projected struct is the C/C++ record it
12+
// mirrors field for field — the transparent layout interop passes by value.
13+
@frozen public struct {{{name}}} {
14+
{{#fields}}
15+
public var {{{name}}}: {{{type}}}
16+
{{/fields}}
17+
18+
// The synthesized memberwise initializer is internal even for a public
19+
// struct, so an explicit public one lets a caller outside the generated
20+
// module construct the value type to pass to a native API.
21+
public init({{#fields}}{{{name}}}{{#aliased}} {{{local}}}{{/aliased}}: {{{type}}}{{^last}}, {{/last}}{{/fields}}) {
22+
{{#fields}}
23+
self.{{{name}}} = {{{local}}}
24+
{{/fields}}
25+
}
26+
}
27+
{{/struct}}
28+
{{#enum}}
29+
{{#flags}}
30+
{{! A `[flags]` enum projects to an OptionSet the way ClangImporter imports an
31+
NS_OPTIONS C enum: its members combine as a bitmask, which a native `enum`
32+
case — a single value — cannot name. `rawValue` is typed as the enum's
33+
`value__` underlying type directly — the OptionSet `RawValue` associated
34+
type is inferred from it rather than spelled as an explicit `typealias`, so
35+
a member legitimately named `RawValue` (not a keyword `SANITIZE` escapes)
36+
stays a `static var` and does not collide with a support typealias. The set
37+
is thus the ABI-exact width of the C enum, and each `@_transparent` member
38+
folds to its raw constant at the use site with no static storage; a repeated
39+
raw value across members is fine here. }}
40+
@frozen public struct {{{name}}}: OptionSet {
41+
public let rawValue: {{{underlying}}}
42+
@inlinable public init(rawValue: {{{underlying}}}) { self.rawValue = rawValue }
43+
{{#members}}
44+
@_transparent public static var {{{name}}}: {{{owner}}} { {{{owner}}}(rawValue: {{{value}}}) }
45+
{{/members}}
46+
}
47+
{{/flags}}
48+
{{^flags}}
49+
// A regular enum projects to an explicitly-stored raw-value struct newtype, not
50+
// a native Swift enum: a native `enum E: {{{underlying}}}` does not carry the ABI
51+
// layout of its raw type — `@frozen` freezes the compact representation Swift
52+
// chooses, and the raw type is only a `RawRepresentable` conversion, so a
53+
// two-case `enum E: UInt32` is one byte, not four, and the signature would pass
54+
// the wrong width to native code. The newtype stores the `value__` underlying
55+
// type directly, so it is the ABI-exact width of the C enum; its members are
56+
// named constants and it holds any ABI value (an undeclared or aliased one)
57+
// where a native enum would be the wrong size and trap.
58+
@frozen public struct {{{name}}}: Hashable, Sendable {
59+
public var rawValue: {{{underlying}}}
60+
@inlinable public init(rawValue: {{{underlying}}}) { self.rawValue = rawValue }
61+
{{#members}}
62+
@_transparent public static var {{{name}}}: {{{owner}}} { {{{owner}}}(rawValue: {{{value}}}) }
63+
{{/members}}
64+
}
65+
{{/flags}}
66+
{{/enum}}
67+
{{#delegate}}
68+
// A delegate is a COM interface — IUnknown plus a single `Invoke` — so `@com`
69+
// generates the IUnknown-based vtable from this Invoke-only protocol, the same
70+
// ABI-faithful shape an interface projects, not a Swift closure.
71+
@com(interface: "{{{iid}}}")
72+
public protocol {{{name}}} {
73+
func Invoke({{#params}}_ {{{name}}}: {{{type}}}{{^last}}, {{/last}}{{/params}}){{#returns}} -> {{{.}}}{{/returns}}
74+
}
75+
{{/delegate}}

0 commit comments

Comments
 (0)