From 780c9eeef027248984f564e4dec1ea04a1dbd70f Mon Sep 17 00:00:00 2001 From: elijahr Date: Mon, 5 Jan 2026 08:21:59 -0600 Subject: [PATCH] fixes #25405; initialization for objects with opaque importc fields (#25406) Objects containing `importc` fields without `completeStruct` fail to compile when used as const/static. The C codegen generates "aggregate initialization" which is invalid for opaque types. Fixes #25405. Nim code: ```nim type OpaqueInt {.importc: "_Atomic int", nodecl.} = object ContainsImportc = object normal: int opaque: OpaqueInt const c = default(ContainsImportc) ``` Resulting C code: ```c // Invalid C - cannot aggregate-init opaque type NIM_CONST ContainsImportc c = {((NI) 0), {}}; ^^ error: illegal initializer type ``` ## Solution Fix in `ccgexprs.nim`: 1. Skip opaque importc fields when building aggregate initializers 2. Use "designated initializers" (`siNamedStruct`) when opaque fields are present to avoid positional misalignment ```c // Valid C: // - opaque field is omitted and implicitly zero-initialized by C // - other fields are explitly named and initialized NIM_CONST ContainsImportc c = {.normal = ((NI) 0)}; ``` This correctly handles the case where the opaque fields might be in any order. A field is considered "opaque importc" if: - Has `sfImportc` flag - Does NOT have `tfCompleteStruct` flag - Either has `tfIncompleteStruct` OR is an object with no visible fields The `containsOpaqueImportcField` proc recursively checks all object fields, including nested objects and variant branches. Anonymous unions (from variant objects) are handled by passing an empty field name, which skips the `.fieldname = ` prefix since C anonymous unions have no field name. Note that initialization for structs without opaque importc fields remains the same as before this changeset. ## Test Coverage `tests/ccgbugs/timportc_field_init.nim` covers: - Simple struct with one importc field - Nested struct containing struct with importc field - Variant object (case object) with importc field in a branch - Array of structs with importc fields - Tuple containing struct with importc field - `completeStruct` importc types (still use aggregate init) - Sandwich case (opaque field between two non-opaque fields) - Fields with different C names (`{.importc: "c_name".}`, `{.exportc.}`) - `{.packed.}` structs with opaque fields - `{.union.}` types with opaque fields - Deep nesting (3+ levels) - Multiple opaque fields with renamed fields between them --- compiler/cbuilderdecls.nim | 12 +- compiler/ccgexprs.nim | 102 ++++++++++++--- tests/ccgbugs/timportc_field_init.nim | 178 ++++++++++++++++++++++++++ 3 files changed, 269 insertions(+), 23 deletions(-) create mode 100644 tests/ccgbugs/timportc_field_init.nim diff --git a/compiler/cbuilderdecls.nim b/compiler/cbuilderdecls.nim index 0b170c7183..eb6dd3d627 100644 --- a/compiler/cbuilderdecls.nim +++ b/compiler/cbuilderdecls.nim @@ -154,14 +154,14 @@ template addField(builder: var Builder, constr: var StructInitializer, name: str # no name, can just add value valueBody of siOrderedStruct: - # no name, can just add value on C - assert name.len != 0, "name has to be given for struct initializer field" + # positional init - name not used in output (empty allowed for anonymous unions) valueBody of siNamedStruct: - assert name.len != 0, "name has to be given for struct initializer field" - builder.add(".") - builder.add(name) - builder.add(" = ") + # designated init - empty name for anonymous unions (skips .name = prefix) + if name.len != 0: + builder.add(".") + builder.add(name) + builder.add(" = ") valueBody proc finishStructInitializer(builder: var Builder, constr: StructInitializer) = diff --git a/compiler/ccgexprs.nim b/compiler/ccgexprs.nim index 2ef134f497..e4e51f65be 100644 --- a/compiler/ccgexprs.nim +++ b/compiler/ccgexprs.nim @@ -3713,6 +3713,64 @@ proc expr(p: BProc, n: PNode, d: var TLoc) = of nkMixinStmt, nkBindStmt, nkReplayAction: discard else: internalError(p.config, n.info, "expr(" & $n.kind & "); unknown node kind") +proc isOpaqueImportcType(t: PType): bool = + # importc type without completeStruct that can't use aggregate init (e.g. C11 _Atomic) + if t.sym != nil and sfImportc in t.sym.flags: + if tfCompleteStruct notin t.flags: + if tfIncompleteStruct in t.flags: + return true + if t.kind == tyObject and (t.n == nil or t.n.len == 0): + return true + return false + +proc containsOpaqueImportcField(typ: PType): bool + +proc containsOpaqueImportcFieldAux(t: PType; n: PNode): bool = + if n == nil: return false + case n.kind + of nkRecList: + for child in n.sons: + if containsOpaqueImportcFieldAux(t, child): + return true + of nkRecCase: + if containsOpaqueImportcFieldAux(t, n[0]): + return true + for i in 1..".} = object + +type + SimpleStruct = object + normal: int + opaque: OpaqueFile + + NestedStruct = object + inner: SimpleStruct + value: float + + VariantStruct = object + case kind: bool + of true: + opaque: OpaqueFile + of false: + normal: int + + ArrayElementStruct = object + id: int + atom: OpaqueFile + +const simple = default(SimpleStruct) +const nested = default(NestedStruct) +const variant = default(VariantStruct) +const arr = default(array[3, ArrayElementStruct]) + +static: + doAssert simple.normal == 0 + doAssert nested.value == 0.0 + doAssert arr[0].id == 0 + +# completeStruct types use normal aggregate init +type CompleteImportc {.importc: "int", completeStruct, nodecl.} = object + value: cint + +type StructWithComplete = object + c: CompleteImportc + x: int + +const withComplete = default(StructWithComplete) + +type TupleWithOpaque = tuple[x: int, s: SimpleStruct, y: float] +const tupleVal = default(TupleWithOpaque) + +# Sandwich: opaque between non-opaque fields requires designated init +type SandwichStruct = object + first: int + opaque: OpaqueFile + last: float + +const sandwich = default(SandwichStruct) + +static: + doAssert withComplete.x == 0 + doAssert tupleVal.x == 0 + doAssert sandwich.first == 0 + doAssert sandwich.last == 0.0 + +proc useSimple(s: ptr SimpleStruct) {.exportc, noinline.} = discard +proc useNested(s: ptr NestedStruct) {.exportc, noinline.} = discard +proc useArr(a: ptr array[3, ArrayElementStruct]) {.exportc, noinline.} = discard +proc useComplete(s: ptr StructWithComplete) {.exportc, noinline.} = discard +proc useVariant(v: ptr VariantStruct) {.exportc, noinline.} = discard +proc useTuple(t: TupleWithOpaque) {.exportc, noinline.} = discard +proc useSandwich(s: ptr SandwichStruct) {.exportc, noinline.} = discard + +useSimple(simple.addr) +useNested(nested.addr) +useArr(arr.addr) +useComplete(withComplete.addr) +useVariant(variant.addr) +useTuple(tupleVal) +useSandwich(sandwich.addr) + +# Edge cases: different C/Nim names +type OpaqueWithCName {.importc: "FILE", header: "".} = object + +type StructWithRenamedField = object + nimName {.importc: "c_name".}: int + opaque: OpaqueWithCName + +const renamedField = default(StructWithRenamedField) +proc useRenamedField(s: ptr StructWithRenamedField) {.exportc, noinline.} = discard +useRenamedField(renamedField.addr) +static: doAssert renamedField.nimName == 0 + +type NimTypeName {.importc: "int", completeStruct, nodecl.} = distinct cint + +type StructContainingRenamedType = object + inner: NimTypeName + opaque: OpaqueFile + +const withRenamedType = default(StructContainingRenamedType) +proc useRenamedType(s: ptr StructContainingRenamedType) {.exportc, noinline.} = discard +useRenamedType(withRenamedType.addr) + +type StructWithExportedField = object + nimField {.exportc: "exported_field".}: int + opaque: OpaqueFile + +const withExported = default(StructWithExportedField) +proc useExportedField(s: ptr StructWithExportedField) {.exportc, noinline.} = discard +useExportedField(withExported.addr) +static: doAssert withExported.nimField == 0 + +type ByCopyStruct {.bycopy.} = object + data: int + opaque: OpaqueFile + +const byCopyVal = default(ByCopyStruct) +proc useByCopy(s: ByCopyStruct) {.exportc, noinline.} = discard +useByCopy(byCopyVal) +static: doAssert byCopyVal.data == 0 + +type PackedStruct {.packed.} = object + a: int8 + opaque: OpaqueFile + b: int8 + +const packedVal = default(PackedStruct) +proc usePacked(s: ptr PackedStruct) {.exportc, noinline.} = discard +usePacked(packedVal.addr) +static: + doAssert packedVal.a == 0 + doAssert packedVal.b == 0 + +type UnionWithOpaque {.union.} = object + intVal: int + opaque: OpaqueFile + +const unionVal = default(UnionWithOpaque) +proc useUnion(u: ptr UnionWithOpaque) {.exportc, noinline.} = discard +useUnion(unionVal.addr) + +# Deep nesting +type DeepLevel1 = object + field1: int + opaque: OpaqueFile + +type DeepLevel2 = object + nested: DeepLevel1 + field2: float + +type DeepLevel3 = object + deep: DeepLevel2 + field3: int + opaque2: OpaqueWithCName + +const deepVal = default(DeepLevel3) +proc useDeep(d: ptr DeepLevel3) {.exportc, noinline.} = discard +useDeep(deepVal.addr) +static: + doAssert deepVal.deep.nested.field1 == 0 + doAssert deepVal.deep.field2 == 0.0 + doAssert deepVal.field3 == 0 + +# Multiple opaque fields with renamed non-opaque fields +type MultiOpaque = object + first {.importc: "first_field".}: int + opaque1: OpaqueFile + second {.importc: "second_field".}: float + opaque2: OpaqueWithCName + third: int + +const multiOpaque = default(MultiOpaque) +proc useMultiOpaque(m: ptr MultiOpaque) {.exportc, noinline.} = discard +useMultiOpaque(multiOpaque.addr) + +static: + doAssert multiOpaque.first == 0 + doAssert multiOpaque.second == 0.0 + doAssert multiOpaque.third == 0