Skip to content

Commit 3ab1e77

Browse files
authored
[native_toolchain_c] Support Module Definitions for linking on Windows (#2386)
1 parent bf64eef commit 3ab1e77

File tree

10 files changed

+194
-24
lines changed

10 files changed

+194
-24
lines changed

pkgs/native_toolchain_c/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.16.7
2+
3+
* Support Module Definitions for linking on Windows.
4+
15
## 0.16.6
26

37
* Support linking for Windows.

pkgs/native_toolchain_c/lib/src/cbuilder/linker_options.dart

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ class LinkerOptions {
3838
/// If null all symbols will be kept.
3939
final List<String>? _symbolsToKeep;
4040

41+
final bool _generateLinkerScript;
42+
4143
/// Create linking options manually for fine-grained control.
4244
LinkerOptions.manual({
4345
List<String>? flags,
@@ -47,7 +49,8 @@ class LinkerOptions {
4749
Iterable<String>? symbolsToKeep,
4850
}) : _linkerFlags = flags ?? [],
4951
gcSections = gcSections ?? true,
50-
_symbolsToKeep = symbolsToKeep?.toList(growable: false);
52+
_symbolsToKeep = symbolsToKeep?.toList(growable: false),
53+
_generateLinkerScript = false;
5154

5255
/// Create linking options to tree-shake symbols from the input files.
5356
///
@@ -59,7 +62,8 @@ class LinkerOptions {
5962
}) : _linkerFlags = flags?.toList(growable: false) ?? [],
6063
_symbolsToKeep = symbols?.toList(growable: false),
6164
gcSections = true,
62-
linkerScript = _createLinkerScript(symbols);
65+
linkerScript = null,
66+
_generateLinkerScript = symbols != null;
6367

6468
Iterable<String> _toLinkerSyntax(Tool linker, Iterable<String> flagList) {
6569
if (linker.isClangLike) {
@@ -70,22 +74,6 @@ class LinkerOptions {
7074
throw UnsupportedError('Linker flags for $linker are not supported');
7175
}
7276
}
73-
74-
static Uri? _createLinkerScript(Iterable<String>? symbols) {
75-
if (symbols == null) return null;
76-
final tempDir = Directory.systemTemp.createTempSync();
77-
final symbolsFileUri = tempDir.uri.resolve('symbols.lds');
78-
final symbolsFile = File.fromUri(symbolsFileUri)..createSync();
79-
symbolsFile.writeAsStringSync('''
80-
{
81-
global:
82-
${symbols.map((e) => '$e;').join('\n ')}
83-
local:
84-
*;
85-
};
86-
''');
87-
return symbolsFileUri;
88-
}
8977
}
9078

9179
extension LinkerOptionsExt on LinkerOptions {
@@ -145,7 +133,9 @@ extension LinkerOptionsExt on LinkerOptions {
145133
if (stripDebug) '--strip-debug',
146134
if (gcSections) '--gc-sections',
147135
if (linkerScript != null)
148-
'--version-script=${linkerScript!.toFilePath()}',
136+
'--version-script=${linkerScript!.toFilePath()}'
137+
else if (_generateLinkerScript && _symbolsToKeep != null)
138+
'--version-script=${_createClangLikeLinkScript(_symbolsToKeep)}',
149139
if (wholeArchiveSandwich) '--no-whole-archive',
150140
]),
151141
];
@@ -169,7 +159,38 @@ extension LinkerOptionsExt on LinkerOptions {
169159
'/INCLUDE:${targetArch == Architecture.ia32 ? '_' : ''}$symbol',
170160
) ??
171161
[],
162+
if (linkerScript != null)
163+
'/DEF:${linkerScript!.toFilePath()}'
164+
else if (_generateLinkerScript && _symbolsToKeep != null)
165+
'/DEF:${_createClLinkScript(_symbolsToKeep)}',
172166
if (stripDebug) '/PDBSTRIPPED',
173167
if (gcSections) '/OPT:REF',
174168
];
169+
170+
static String _createClangLikeLinkScript(Iterable<String> symbols) {
171+
final tempDir = Directory.systemTemp.createTempSync();
172+
final symbolsFileUri = tempDir.uri.resolve('symbols.lds');
173+
final symbolsFile = File.fromUri(symbolsFileUri)..createSync();
174+
symbolsFile.writeAsStringSync('''
175+
{
176+
global:
177+
${symbols.map((e) => '$e;').join('\n ')}
178+
local:
179+
*;
180+
};
181+
''');
182+
return symbolsFileUri.toFilePath();
183+
}
184+
185+
static String _createClLinkScript(Iterable<String> symbols) {
186+
final tempDir = Directory.systemTemp.createTempSync();
187+
final symbolsFileUri = tempDir.uri.resolve('symbols.def');
188+
final symbolsFile = File.fromUri(symbolsFileUri)..createSync();
189+
symbolsFile.writeAsStringSync('''
190+
LIBRARY MyDLL
191+
EXPORTS
192+
${symbols.map((s) => ' $s').join('\n')}
193+
''');
194+
return symbolsFileUri.toFilePath();
195+
}
175196
}

pkgs/native_toolchain_c/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: native_toolchain_c
22
description: >-
33
A library to invoke the native C compiler installed on the host machine.
4-
version: 0.16.6
4+
version: 0.16.7
55
repository: https://github.com/dart-lang/native/tree/main/pkgs/native_toolchain_c
66

77
topics:

pkgs/native_toolchain_c/test/clinker/build_testfiles.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Future<Uri> buildTestArchive(
1515
Uri tempUri2,
1616
OS targetOS,
1717
Architecture architecture, {
18+
List<Uri>? extraSources,
1819
int? androidTargetNdkApi, // Must be specified iff targetOS is OS.android.
1920
int? macOSTargetVersion, // Must be specified iff targetOS is OS.macos.
2021
int? iOSTargetVersion, // Must be specified iff targetOS is OS.iOS.
@@ -77,7 +78,11 @@ Future<Uri> buildTestArchive(
7778
final cbuilder = CBuilder.library(
7879
name: name,
7980
assetName: '',
80-
sources: [test1Uri.toFilePath(), test2Uri.toFilePath()],
81+
sources: [
82+
test1Uri.toFilePath(),
83+
test2Uri.toFilePath(),
84+
...?extraSources?.map((src) => src.toFilePath()),
85+
],
8186
linkModePreference: LinkModePreference.static,
8287
buildMode: BuildMode.release,
8388
);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
LIBRARY MyDLL
2+
EXPORTS
3+
my_other_func
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#include <stdio.h>
2+
3+
// Explicitly not marked for export.
4+
void my_unexported_func()
5+
{
6+
printf("Hello unexported World");
7+
}

pkgs/native_toolchain_c/test/clinker/treeshake_helper.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ void runTreeshakeTests(
3939
symbolsToKeep: ['my_other_func'],
4040
stripDebug: true,
4141
gcSections: true,
42-
linkerScript: packageUri.resolve(
43-
'test/clinker/testfiles/linker/symbols.lds',
44-
),
42+
linkerScript: targetOS == OS.windows
43+
? packageUri.resolve('test/clinker/testfiles/linker/symbols.def')
44+
: packageUri.resolve('test/clinker/testfiles/linker/symbols.lds'),
4545
),
4646
);
4747
CLinker linkerAuto(List<String> sources) => CLinker.library(
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
@TestOn('windows')
6+
library;
7+
8+
import 'dart:io';
9+
10+
import 'package:code_assets/code_assets.dart';
11+
import 'package:test/test.dart';
12+
13+
import '../helpers.dart';
14+
import 'windows_module_definition_helper.dart';
15+
16+
void main() {
17+
if (!Platform.isWindows) {
18+
// Avoid needing status files on Dart SDK CI.
19+
return;
20+
}
21+
22+
final architectures = supportedArchitecturesFor(OS.current)
23+
// See windows_module_definition_test.dart for current arch.
24+
..remove(Architecture.current);
25+
runWindowsModuleDefinitionTests(architectures);
26+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:io';
6+
7+
import 'package:code_assets/code_assets.dart';
8+
import 'package:hooks/hooks.dart';
9+
import 'package:native_toolchain_c/native_toolchain_c.dart';
10+
import 'package:test/test.dart';
11+
12+
import '../helpers.dart';
13+
import 'build_testfiles.dart';
14+
15+
void runWindowsModuleDefinitionTests(List<Architecture> architectures) {
16+
const name = 'mylibname';
17+
const targetOS = OS.windows;
18+
19+
for (final architecture in architectures) {
20+
test(
21+
'Module definition script can export unexported function ($architecture)',
22+
() async {
23+
final tempUri = await tempDirForTest();
24+
final tempUri2 = await tempDirForTest();
25+
26+
final uri = await buildTestArchive(
27+
tempUri,
28+
tempUri2,
29+
targetOS,
30+
architecture,
31+
extraSources: [
32+
packageUri.resolve('test/clinker/testfiles/linker/test3.c'),
33+
],
34+
);
35+
36+
final linkInputBuilder = LinkInputBuilder()
37+
..setupShared(
38+
packageName: 'testpackage',
39+
packageRoot: tempUri,
40+
outputFile: tempUri.resolve('output.json'),
41+
outputDirectoryShared: tempUri2,
42+
)
43+
..setupLink(assets: [], recordedUsesFile: null)
44+
..addExtension(
45+
CodeAssetExtension(
46+
targetOS: targetOS,
47+
targetArchitecture: architecture,
48+
linkModePreference: LinkModePreference.dynamic,
49+
cCompiler: cCompiler,
50+
),
51+
);
52+
53+
final linkInput = linkInputBuilder.build();
54+
final linkOutput = LinkOutputBuilder();
55+
56+
printOnFailure(linkInput.config.code.cCompiler.toString());
57+
printOnFailure(Platform.environment.keys.toList().toString());
58+
await CLinker.library(
59+
name: name,
60+
assetName: '',
61+
linkerOptions: LinkerOptions.treeshake(
62+
symbols: ['my_func', 'my_unexported_func'],
63+
),
64+
sources: [uri.toFilePath()],
65+
).run(input: linkInput, output: linkOutput, logger: logger);
66+
67+
final codeAssets = LinkOutput(linkOutput.json).assets.code;
68+
expect(codeAssets, hasLength(1));
69+
final asset = codeAssets.first;
70+
expect(asset, isA<CodeAsset>());
71+
final symbols = await readSymbols(asset, targetOS);
72+
final skipReason = symbols == null
73+
? 'tool to extract symbols unavailable'
74+
: false;
75+
expect(symbols, contains('my_func'), skip: skipReason);
76+
// Module Definition file causes my_unexported_func to be exported even
77+
// though it wasn't marked for export.
78+
expect(symbols, contains('my_unexported_func'), skip: skipReason);
79+
expect(symbols, isNot(contains('my_other_func')), skip: skipReason);
80+
},
81+
);
82+
}
83+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
@TestOn('windows')
6+
library;
7+
8+
import 'dart:io';
9+
10+
import 'package:code_assets/code_assets.dart';
11+
import 'package:test/test.dart';
12+
13+
import 'windows_module_definition_helper.dart';
14+
15+
void main() {
16+
if (!Platform.isWindows) {
17+
// Avoid needing status files on Dart SDK CI.
18+
return;
19+
}
20+
runWindowsModuleDefinitionTests([Architecture.current]);
21+
}

0 commit comments

Comments
 (0)