未验证 提交 80f015da 编写于 作者: A Ankit Jain 提交者: GitHub

[wasm] Build improvements based on feedback (#59391)

- Fix for a incremental build issue
- Change native build defaults for `Debug` config, to make it faster (thanks @SteveSandersonMS, @mattleibow)

- MonoAOTCompiler:
  - Skip unmanaged assemblies, and emit a warning
- PInvokeTableGenerator:
  - Fix to not fail in presence of native variadic functions, and pinvokes with function pointers (eg. used by sqlite)
Co-authored-by: NLarry Ewing <lewing@microsoft.com>
上级 d8e2d11a
......@@ -166,7 +166,7 @@
<SQLitePCLRawbundle_greenVersion>2.0.4</SQLitePCLRawbundle_greenVersion>
<MoqVersion>4.12.0</MoqVersion>
<FsCheckVersion>2.14.3</FsCheckVersion>
<SdkVersionForWorkloadTesting>6.0.100-rc.2.21463.12</SdkVersionForWorkloadTesting>
<SdkVersionForWorkloadTesting>6.0.100-rc.2.21474.31</SdkVersionForWorkloadTesting>
<!-- Docs -->
<MicrosoftPrivateIntellisenseVersion>6.0.0-preview-20210916.1</MicrosoftPrivateIntellisenseVersion>
<!-- ILLink -->
......
......@@ -11,6 +11,7 @@ Wasm.Build.Tests.LocalEMSDKTests
Wasm.Build.Tests.MainWithArgsTests
Wasm.Build.Tests.NativeBuildTests
Wasm.Build.Tests.NativeLibraryTests
Wasm.Build.Tests.PInvokeTableGeneratorTests
Wasm.Build.Tests.RebuildTests
Wasm.Build.Tests.SatelliteAssembliesTests
Wasm.Build.Tests.WasmBuildAppTest
......
......@@ -16,7 +16,6 @@
</ItemGroup>
<Copy SourceFiles="@(_SourceFiles)" DestinationFolder="$(SdkWithWorkloadForTestingPath)\%(_SourceFiles.RecursiveDir)" />
<Copy SourceFiles="$(MonoProjectRoot)\wasm\BlazorOverwrite.targets" DestinationFiles="$(SdkWithWorkloadForTestingPath)\sdk\$(SdkVersionForWorkloadTesting)\Sdks\Microsoft.NET.Sdk.BlazorWebAssembly\targets\Microsoft.NET.Sdk.BlazorWebAssembly.6_0.targets" />
<WriteLinesToFile File="$(SdkWithWorkloadStampPath)" Lines="" Overwrite="true" />
</Target>
......@@ -41,7 +40,6 @@
<Exec Condition="$([MSBuild]::IsOSPlatform('windows'))"
Command='powershell -ExecutionPolicy ByPass -NoProfile -command "&amp; $(_DotNetInstallScriptPath) -InstallDir $(SdkWithNoWorkloadForTestingPath) -Version $(SdkVersionForWorkloadTesting)"' />
<Copy SourceFiles="$(MonoProjectRoot)\wasm\BlazorOverwrite.targets" DestinationFiles="$(SdkWithNoWorkloadForTestingPath)\sdk\$(SdkVersionForWorkloadTesting)\Sdks\Microsoft.NET.Sdk.BlazorWebAssembly\targets\Microsoft.NET.Sdk.BlazorWebAssembly.6_0.targets" />
<WriteLinesToFile File="$(SdkWithNoWorkloadStampPath)" Lines="" Overwrite="true" />
</Target>
......
此差异已折叠。
......@@ -143,6 +143,7 @@
<PropertyGroup>
<_MonoAotCrossCompilerPath>@(MonoAotCrossCompiler->WithMetadataValue('RuntimeIdentifier','browser-wasm'))</_MonoAotCrossCompilerPath>
<_EmccDefaultFlagsRsp>$([MSBuild]::NormalizePath($(_WasmRuntimePackSrcDir), 'emcc-default.rsp'))</_EmccDefaultFlagsRsp>
<WasmNativeStrip Condition="'$(WasmNativeStrip)' == '' and '$(Configuration)' == 'Debug' and '$(WasmBuildingForNestedPublish)' != 'true'">false</WasmNativeStrip>
<WasmNativeStrip Condition="'$(WasmNativeStrip)' == ''">true</WasmNativeStrip>
<WasmNativeDebugSymbols Condition="'$(WasmNativeDebugSymbols)' == ''">true</WasmNativeDebugSymbols>
<WasmLinkIcalls Condition="'$(WasmLinkIcalls)' == ''">$(WasmBuildNative)</WasmLinkIcalls>
......@@ -156,8 +157,7 @@
<_EmccAssertionLevelDefault>0</_EmccAssertionLevelDefault>
<_EmccOptimizationFlagDefault Condition="'$(_WasmDevel)' == 'true'">-O0 -s ASSERTIONS=$(_EmccAssertionLevelDefault)</_EmccOptimizationFlagDefault>
<_EmccOptimizationFlagDefault Condition="'$(_EmccOptimizationFlagDefault)' == '' and '$(OS)' != 'Windows_NT' and '$(Configuration)' == 'Debug'">-Os</_EmccOptimizationFlagDefault>
<_EmccOptimizationFlagDefault Condition="'$(_EmccOptimizationFlagDefault)' == '' and '$(Configuration)' != 'Debug'">-Oz</_EmccOptimizationFlagDefault>
<_EmccOptimizationFlagDefault Condition="'$(_EmccOptimizationFlagDefault)' == '' and '$(Configuration)' == 'Debug' and '$(WasmBuildingForNestedPublish)' != 'true'">-O1</_EmccOptimizationFlagDefault>
<_EmccOptimizationFlagDefault Condition="'$(_EmccOptimizationFlagDefault)' == ''">-Oz</_EmccOptimizationFlagDefault>
<EmccCompileOptimizationFlag Condition="'$(EmccCompileOptimizationFlag)' == ''">$(_EmccOptimizationFlagDefault)</EmccCompileOptimizationFlag>
......@@ -206,6 +206,9 @@
<_EmccLDFlags Include="@(_EmccCommonFlags)" />
<_EmccLDFlags Include="-s TOTAL_MEMORY=$(EmccTotalMemory)" />
<!-- ILLinker should have removed unused imports, so error for Publish -->
<_EmccLDFlags Include="-s ERROR_ON_UNDEFINED_SYMBOLS=0" Condition="'$(WasmBuildingForNestedPublish)' != 'true'" />
<_DriverCDependencies Include="$(_WasmPInvokeHPath);$(_WasmICallTablePath)" />
<_DriverCDependencies Include="$(_DriverGenCPath)" Condition="'$(_DriverGenCNeeded)' == 'true'" />
......@@ -297,7 +300,8 @@
Inputs="@(_BitcodeFile);$(_EmccDefaultFlagsRsp);$(_EmccCompileBitcodeRsp)"
Outputs="@(_BitcodeFile->'%(ObjectFile)')"
Condition="'$(_WasmShouldAOT)' == 'true' and @(_BitcodeFile->Count()) > 0"
DependsOnTargets="_WasmWriteRspForCompilingBitcode">
DependsOnTargets="_WasmWriteRspForCompilingBitcode"
Returns="@(FileWrites)">
<ItemGroup>
<_BitCodeFile Dependencies="%(_BitCodeFile.Dependencies);$(_EmccDefaultFlagsRsp);$(_EmccCompileBitcodeRsp)" />
......@@ -363,7 +367,8 @@
<Target Name="_WasmLinkDotNet"
Inputs="@(_WasmLinkDependencies);$(_EmccDefaultFlagsRsp);$(_EmccLinkRsp)"
Outputs="$(_WasmIntermediateOutputPath)dotnet.js;$(_WasmIntermediateOutputPath)dotnet.wasm"
DependsOnTargets="_WasmSelectRuntimeComponentsForLinking;_WasmCompileAssemblyBitCodeFilesForAOT;_WasmWriteRspFilesForLinking">
DependsOnTargets="_WasmSelectRuntimeComponentsForLinking;_WasmCompileAssemblyBitCodeFilesForAOT;_WasmWriteRspFilesForLinking"
Returns="@(FileWrites)" >
<Message Text="Linking with emcc. This may take a while ..." Importance="High" />
<Message Text="Running emcc with @(_EmccLinkStepArgs->'%(Identity)', ' ')" Importance="Low" />
......
......@@ -325,6 +325,13 @@ private bool ProcessAndValidateArguments()
throw new LogAsErrorException($"'{nameof(OutputType)}=Library' can not be used with '{nameof(UseStaticLinking)}=true'.");
}
foreach (var asmItem in Assemblies)
{
string? fullPath = asmItem.GetMetadata("FullPath");
if (!File.Exists(fullPath))
throw new LogAsErrorException($"Could not find {fullPath} to AOT");
}
return !Log.HasLoggedErrors;
}
......@@ -339,6 +346,12 @@ public override bool Execute()
Log.LogError(laee.Message);
return false;
}
finally
{
if (_cache != null && _cache.Save(CacheFilePath!))
_fileWrites.Add(CacheFilePath!);
FileWrites = _fileWrites.ToArray();
}
}
private bool ExecuteInternal()
......@@ -347,6 +360,7 @@ private bool ExecuteInternal()
return false;
_assembliesToCompile = EnsureAndGetAssembliesInTheSameDir(Assemblies);
_assembliesToCompile = FilterAssemblies(_assembliesToCompile);
if (!string.IsNullOrEmpty(AotModulesTablePath) && !GenerateAotModulesTable(_assembliesToCompile, Profilers, AotModulesTablePath))
return false;
......@@ -372,42 +386,29 @@ private bool ExecuteInternal()
}
else
{
int allowedParallelism = Math.Min(_assembliesToCompile.Count, Environment.ProcessorCount);
int allowedParallelism = DisableParallelAot ? 1 : Math.Min(_assembliesToCompile.Count, Environment.ProcessorCount);
if (BuildEngine is IBuildEngine9 be9)
allowedParallelism = be9.RequestCores(allowedParallelism);
if (DisableParallelAot || allowedParallelism == 1)
ParallelLoopResult result = Parallel.ForEach(
argsList,
new ParallelOptions { MaxDegreeOfParallelism = allowedParallelism },
(args, state) => PrecompileLibraryParallel(args, state));
Log.LogMessage(MessageImportance.High, $"result: {result.IsCompleted}");
if (result.IsCompleted)
{
foreach (var args in argsList)
{
if (!PrecompileLibrarySerial(args))
return !Log.HasLoggedErrors;
}
int numUnchanged = _totalNumAssemblies - _numCompiled;
if (numUnchanged > 0 && numUnchanged != _totalNumAssemblies)
Log.LogMessage(MessageImportance.High, $"[{numUnchanged}/{_totalNumAssemblies}] skipped unchanged assemblies.");
}
else
else if (!Log.HasLoggedErrors)
{
ParallelLoopResult result = Parallel.ForEach(
argsList,
new ParallelOptions { MaxDegreeOfParallelism = allowedParallelism },
(args, state) => PrecompileLibraryParallel(args, state));
if (!result.IsCompleted)
{
return false;
}
Log.LogError($"Precompiling failed due to unknown reasons. Check log for more info");
}
int numUnchanged = _totalNumAssemblies - _numCompiled;
if (numUnchanged > 0 && numUnchanged != _totalNumAssemblies)
Log.LogMessage(MessageImportance.High, $"[{numUnchanged}/{_totalNumAssemblies}] skipped unchanged assemblies.");
}
CompiledAssemblies = ConvertAssembliesDictToOrderedList(compiledAssemblies, _assembliesToCompile).ToArray();
if (_cache.Save(CacheFilePath!))
_fileWrites.Add(CacheFilePath!);
FileWrites = _fileWrites.ToArray();
return !Log.HasLoggedErrors;
}
......@@ -428,31 +429,44 @@ static bool IsNewerThanOutput(string inFile, string outFile)
(File.GetLastWriteTimeUtc(inFile) > File.GetLastWriteTimeUtc(outFile));
}
private IList<ITaskItem> EnsureAndGetAssembliesInTheSameDir(ITaskItem[] originalAssemblies)
private IList<ITaskItem> FilterAssemblies(IEnumerable<ITaskItem> assemblies)
{
List<ITaskItem> filteredAssemblies = new();
string firstAsmDir = Path.GetDirectoryName(originalAssemblies[0].GetMetadata("FullPath")) ?? string.Empty;
bool allInSameDir = true;
foreach (var origAsm in originalAssemblies)
foreach (var asmItem in assemblies)
{
if (allInSameDir && Path.GetDirectoryName(origAsm.GetMetadata("FullPath")) != firstAsmDir)
allInSameDir = false;
if (ShouldSkip(origAsm))
if (ShouldSkip(asmItem))
{
if (parsedAotMode == MonoAotMode.LLVMOnly)
throw new LogAsErrorException($"Building in AOTMode=LLVMonly is not compatible with excluding any assemblies for AOT. Excluded assembly: {origAsm.ItemSpec}");
throw new LogAsErrorException($"Building in AOTMode=LLVMonly is not compatible with excluding any assemblies for AOT. Excluded assembly: {asmItem.ItemSpec}");
Log.LogMessage(MessageImportance.Low, $"Skipping {origAsm.ItemSpec} because it has %(AOT_InternalForceToInterpret)=true");
Log.LogMessage(MessageImportance.Low, $"Skipping {asmItem.ItemSpec} because it has %(AOT_InternalForceToInterpret)=true");
continue;
}
filteredAssemblies.Add(origAsm);
string assemblyPath = asmItem.GetMetadata("FullPath");
using var assemblyFile = File.OpenRead(assemblyPath);
using PEReader reader = new(assemblyFile, PEStreamOptions.Default);
if (!reader.HasMetadata)
{
Log.LogWarning($"Skipping unmanaged {assemblyPath} for AOT");
continue;
}
filteredAssemblies.Add(asmItem);
}
return filteredAssemblies;
static bool ShouldSkip(ITaskItem asmItem)
=> bool.TryParse(asmItem.GetMetadata("AOT_InternalForceToInterpret"), out bool skip) && skip;
}
private IList<ITaskItem> EnsureAndGetAssembliesInTheSameDir(IList<ITaskItem> assemblies)
{
string firstAsmDir = Path.GetDirectoryName(assemblies.First().GetMetadata("FullPath")) ?? string.Empty;
bool allInSameDir = assemblies.All(asm => Path.GetDirectoryName(asm.GetMetadata("FullPath")) == firstAsmDir);
if (allInSameDir)
return filteredAssemblies;
return assemblies;
// Copy to aot-in
......@@ -460,28 +474,23 @@ private IList<ITaskItem> EnsureAndGetAssembliesInTheSameDir(ITaskItem[] original
Directory.CreateDirectory(aotInPath);
List<ITaskItem> newAssemblies = new();
foreach (var origAsm in originalAssemblies)
foreach (var asmItem in assemblies)
{
string asmPath = origAsm.GetMetadata("FullPath");
string asmPath = asmItem.GetMetadata("FullPath");
string newPath = Path.Combine(aotInPath, Path.GetFileName(asmPath));
// FIXME: delete files not in originalAssemblies though
// FIXME: or .. just delete the whole dir?
if (Utils.CopyIfDifferent(asmPath, newPath, useHash: true))
Log.LogMessage(MessageImportance.Low, $"Copying {asmPath} to {newPath}");
_fileWrites.Add(newPath);
if (!ShouldSkip(origAsm))
{
ITaskItem newAsm = new TaskItem(newPath);
origAsm.CopyMetadataTo(newAsm);
newAssemblies.Add(newAsm);
}
ITaskItem newAsm = new TaskItem(newPath);
asmItem.CopyMetadataTo(newAsm);
newAssemblies.Add(newAsm);
}
return newAssemblies;
static bool ShouldSkip(ITaskItem asmItem)
=> bool.TryParse(asmItem.GetMetadata("AOT_InternalForceToInterpret"), out bool skip) && skip;
}
private PrecompileArguments GetPrecompileArgumentsFor(ITaskItem assemblyItem, string? monoPaths)
......@@ -773,28 +782,6 @@ private bool PrecompileLibrary(PrecompileArguments args)
return true;
}
private bool PrecompileLibrarySerial(PrecompileArguments args)
{
try
{
if (PrecompileLibrary(args))
return true;
}
catch (LogAsErrorException laee)
{
Log.LogError($"Precompile failed for {args.AOTAssembly}: {laee.Message}");
}
catch (Exception ex)
{
if (Log.HasLoggedErrors)
Log.LogMessage(MessageImportance.Low, $"Precompile failed for {args.AOTAssembly}: {ex}");
else
Log.LogError($"Precompile failed for {args.AOTAssembly}: {ex}");
}
return false;
}
private void PrecompileLibraryParallel(PrecompileArguments args, ParallelLoopState state)
{
try
......@@ -804,14 +791,14 @@ private void PrecompileLibraryParallel(PrecompileArguments args, ParallelLoopSta
}
catch (LogAsErrorException laee)
{
Log.LogError($"Precompile failed for {args.AOTAssembly}: {laee.Message}");
Log.LogError($"Precompiling failed for {args.AOTAssembly}: {laee.Message}");
}
catch (Exception ex)
{
if (Log.HasLoggedErrors)
Log.LogMessage(MessageImportance.Low, $"Precompile failed for {args.AOTAssembly}: {ex}");
else
Log.LogError($"Precompile failed for {args.AOTAssembly}: {ex}");
Log.LogError($"Precompiling failed for {args.AOTAssembly}: {ex}");
}
state.Break();
......@@ -940,10 +927,8 @@ private static IList<ITaskItem> ConvertAssembliesDictToOrderedList(ConcurrentDic
List<ITaskItem> outItems = new(originalAssemblies.Count);
foreach (ITaskItem item in originalAssemblies)
{
if (!dict.TryGetValue(item.GetMetadata("FullPath"), out ITaskItem? dictItem))
throw new LogAsErrorException($"Bug: Could not find item in the dict with key {item.ItemSpec}");
outItems.Add(dictItem);
if (dict.TryGetValue(item.GetMetadata("FullPath"), out ITaskItem? dictItem))
outItems.Add(dictItem);
}
return outItems;
}
......@@ -993,7 +978,7 @@ public FileCache(string? cacheFilePath, TaskLoggingHelper log)
}
_oldCache ??= new();
_newCache = new();
_newCache = new(_oldCache.FileHashes);
}
public bool ShouldCopy(ProxyFile proxyFile, [NotNullWhen(true)] out string? cause)
......@@ -1110,6 +1095,10 @@ public enum MonoAotModulesTableLanguage
internal class CompilerCache
{
public CompilerCache() => FileHashes = new();
public CompilerCache(IDictionary<string, string> oldHashes)
=> FileHashes = new(oldHashes);
[JsonPropertyName("file_hashes")]
public ConcurrentDictionary<string, string> FileHashes { get; set; } = new();
public ConcurrentDictionary<string, string> FileHashes { get; set; }
}
......@@ -95,6 +95,7 @@ private bool ExecuteActual()
if (!ShouldCompile(srcFile, objFile, depFiles, out string reason))
{
Log.LogMessage(MessageImportance.Low, $"Skipping {srcFile} because {reason}.");
outputItems.Add(CreateOutputItemFor(srcFile, objFile));
}
else
{
......@@ -107,7 +108,8 @@ private bool ExecuteActual()
if (_numCompiled == _totalFiles)
{
// nothing to do!
return true;
OutputFiles = outputItems.ToArray();
return !Log.HasLoggedErrors;
}
if (_numCompiled > 0)
......@@ -123,31 +125,20 @@ private bool ExecuteActual()
_tempPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
Directory.CreateDirectory(_tempPath);
int allowedParallelism = Math.Min(SourceFiles.Length, Environment.ProcessorCount);
int allowedParallelism = DisableParallelCompile ? 1 : Math.Min(SourceFiles.Length, Environment.ProcessorCount);
if (BuildEngine is IBuildEngine9 be9)
allowedParallelism = be9.RequestCores(allowedParallelism);
if (DisableParallelCompile || allowedParallelism == 1)
{
foreach ((string srcFile, string outFile) in filesToCompile)
{
if (!ProcessSourceFile(srcFile, outFile))
return false;
}
}
else
ParallelLoopResult result = Parallel.ForEach(filesToCompile,
new ParallelOptions { MaxDegreeOfParallelism = allowedParallelism },
(toCompile, state) =>
{
ParallelLoopResult result = Parallel.ForEach(filesToCompile,
new ParallelOptions { MaxDegreeOfParallelism = allowedParallelism },
(toCompile, state) =>
{
if (!ProcessSourceFile(toCompile.Item1, toCompile.Item2))
state.Stop();
});
if (!ProcessSourceFile(toCompile.Item1, toCompile.Item2))
state.Stop();
});
if (!result.IsCompleted && !Log.HasLoggedErrors)
Log.LogError("Unknown failure occured while compiling. Check logs to get more details.");
}
if (!result.IsCompleted && !Log.HasLoggedErrors)
Log.LogError("Unknown failure occured while compiling. Check logs to get more details.");
if (!Log.HasLoggedErrors)
{
......@@ -200,9 +191,7 @@ bool ProcessSourceFile(string srcFile, string objFile)
else
Log.LogMessage(MessageImportance.Low, $"Copied {tmpObjFile} to {objFile}");
ITaskItem newItem = new TaskItem(objFile);
newItem.SetMetadata("SourceFile", srcFile);
outputItems.Add(newItem);
outputItems.Add(CreateOutputItemFor(srcFile, objFile));
int count = Interlocked.Increment(ref _numCompiled);
Log.LogMessage(MessageImportance.High, $"[{count}/{_totalFiles}] {Path.GetFileName(srcFile)} -> {Path.GetFileName(objFile)} [took {elapsedSecs:F}s]");
......@@ -219,6 +208,13 @@ bool ProcessSourceFile(string srcFile, string objFile)
File.Delete(tmpObjFile);
}
}
ITaskItem CreateOutputItemFor(string srcFile, string objFile)
{
ITaskItem newItem = new TaskItem(objFile);
newItem.SetMetadata("SourceFile", srcFile);
return newItem;
}
}
private bool ShouldCompile(string srcFile, string objFile, string[] depFiles, out string reason)
......
......@@ -45,7 +45,7 @@ public void GenIcallTable(string runtimeIcallTableFile, string[] assemblies)
{
ReadTable (runtimeIcallTableFile);
var resolver = new PathAssemblyResolver(assemblies);
var mlc = new MetadataLoadContext(resolver, "System.Private.CoreLib");
using var mlc = new MetadataLoadContext(resolver, "System.Private.CoreLib");
foreach (var aname in assemblies)
{
var a = mlc.LoadFromAssemblyPath(aname);
......@@ -107,11 +107,8 @@ private void EmitTable (StreamWriter w)
// Read the icall table generated by mono --print-icall-table
private void ReadTable (string filename)
{
JsonDocument json;
using (var stream = File.Open (filename, FileMode.Open))
{
json = JsonDocument.Parse (stream);
}
using var stream = File.OpenRead (filename);
using JsonDocument json = JsonDocument.Parse (stream);
var arr = json.RootElement;
foreach (var v in arr.EnumerateArray ())
......
......@@ -3,13 +3,10 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Reflection;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
......@@ -65,7 +62,7 @@ public void GenPInvokeTable(string[] pinvokeModules, string[] assemblies)
var callbacks = new List<PInvokeCallback>();
var resolver = new PathAssemblyResolver(assemblies);
var mlc = new MetadataLoadContext(resolver, "System.Private.CoreLib");
using var mlc = new MetadataLoadContext(resolver, "System.Private.CoreLib");
foreach (var aname in assemblies)
{
var a = mlc.LoadFromAssemblyPath(aname);
......@@ -121,25 +118,40 @@ private void EmitPInvokeTable(StreamWriter w, Dictionary<string, string> modules
w.WriteLine("// GENERATED FILE, DO NOT MODIFY");
w.WriteLine();
var decls = new HashSet<string>();
foreach (var pinvoke in pinvokes.OrderBy(l => l.EntryPoint))
var pinvokesGroupedByEntryPoint = pinvokes
.Where(l => modules.ContainsKey(l.Module))
.OrderBy(l => l.EntryPoint)
.GroupBy(l => l.EntryPoint);
var comparer = new PInvokeComparer();
foreach (IGrouping<string, PInvoke> group in pinvokesGroupedByEntryPoint)
{
if (modules.ContainsKey(pinvoke.Module)) {
try
{
var decl = GenPInvokeDecl(pinvoke);
if (decls.Contains(decl))
continue;
var candidates = group.Distinct(comparer).ToArray();
PInvoke first = candidates[0];
if (ShouldTreatAsVariadic(candidates))
{
string imports = string.Join(Environment.NewLine,
candidates.Select(
p => $" {p.Method} (in [{p.Method.DeclaringType?.Assembly.GetName().Name}] {p.Method.DeclaringType})"));
Log.LogWarning($"Found a native function ({first.EntryPoint}) with varargs in {first.Module}." +
" Calling such functions is not supported, and will fail at runtime." +
$" Managed DllImports: {Environment.NewLine}{imports}");
w.WriteLine(decl);
decls.Add(decl);
}
catch (NotSupportedException)
{
// See the FIXME in GenPInvokeDecl
Log.LogWarning($"Cannot handle function pointer arguments/return value in pinvoke method '{pinvoke.Method}' in type '{pinvoke.Method.DeclaringType}'.");
pinvoke.Skip = true;
}
foreach (var c in candidates)
c.Skip = true;
continue;
}
var decls = new HashSet<string>();
foreach (var candidate in candidates)
{
var decl = GenPInvokeDecl(candidate);
if (decl == null || decls.Contains(decl))
continue;
w.WriteLine(decl);
decls.Add(decl);
}
}
......@@ -186,6 +198,22 @@ static string ModuleNameToId(string name)
return fixedName;
}
static bool ShouldTreatAsVariadic(PInvoke[] candidates)
{
if (candidates.Length < 2)
return false;
PInvoke first = candidates[0];
if (TryIsMethodGetParametersUnsupported(first.Method, out _))
return false;
int firstNumArgs = first.Method.GetParameters().Length;
return candidates
.Skip(1)
.Any(c => !TryIsMethodGetParametersUnsupported(c.Method, out _) &&
c.Method.GetParameters().Length != firstNumArgs);
}
}
private string MapType (Type t)
......@@ -205,7 +233,29 @@ private string MapType (Type t)
return "int";
}
private string GenPInvokeDecl(PInvoke pinvoke)
// FIXME: System.Reflection.MetadataLoadContext can't decode function pointer types
// https://github.com/dotnet/runtime/issues/43791
private static bool TryIsMethodGetParametersUnsupported(MethodInfo method, [NotNullWhen(true)] out string? reason)
{
try
{
method.GetParameters();
}
catch (NotSupportedException nse)
{
reason = nse.Message;
return true;
}
catch
{
// not concerned with other exceptions
}
reason = null;
return false;
}
private string? GenPInvokeDecl(PInvoke pinvoke)
{
var sb = new StringBuilder();
var method = pinvoke.Method;
......@@ -215,6 +265,14 @@ private string GenPInvokeDecl(PInvoke pinvoke)
sb.Append($"int {pinvoke.EntryPoint} (int, int, int, int, int);");
return sb.ToString();
}
if (TryIsMethodGetParametersUnsupported(pinvoke.Method, out string? reason))
{
Log.LogWarning($"Skipping the following DllImport because '{reason}'. {Environment.NewLine} {pinvoke.Method}");
pinvoke.Skip = true;
return null;
}
sb.Append(MapType(method.ReturnType));
sb.Append($" {pinvoke.EntryPoint} (");
int pindex = 0;
......@@ -366,7 +424,7 @@ private static bool IsBlittable (Type type)
private static void Error (string msg) => throw new LogAsErrorException(msg);
}
internal class PInvoke
internal class PInvoke : IEquatable<PInvoke>
{
public PInvoke(string entryPoint, string module, MethodInfo method)
{
......@@ -379,6 +437,30 @@ public PInvoke(string entryPoint, string module, MethodInfo method)
public string Module;
public MethodInfo Method;
public bool Skip;
public bool Equals(PInvoke? other)
=> other != null &&
string.Equals(EntryPoint, other.EntryPoint, StringComparison.Ordinal) &&
string.Equals(Module, other.Module, StringComparison.Ordinal) &&
string.Equals(Method.ToString(), other.Method.ToString(), StringComparison.Ordinal);
public override string ToString() => $"{{ EntryPoint: {EntryPoint}, Module: {Module}, Method: {Method}, Skip: {Skip} }}";
}
internal class PInvokeComparer : IEqualityComparer<PInvoke>
{
public bool Equals(PInvoke? x, PInvoke? y)
{
if (x == null && y == null)
return true;
if (x == null || y == null)
return false;
return x.Equals(y);
}
public int GetHashCode(PInvoke pinvoke)
=> $"{pinvoke.EntryPoint}{pinvoke.Module}{pinvoke.Method}".GetHashCode();
}
internal class PInvokeCallback
......
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Xunit;
using Xunit.Abstractions;
......@@ -171,6 +171,60 @@ public void WithNativeReference_AOTOnCommandLine(string config)
BlazorPublish(id, config, expectedFileType: NativeFilesType.Relinked);
}
[ConditionalTheory(typeof(BuildTestBase), nameof(IsUsingWorkloads))]
[InlineData("Debug")]
[InlineData("Release")]
public void WithDllImportInMainAssembly(string config)
{
// Based on https://github.com/dotnet/runtime/issues/59255
string id = $"blz_dllimp_{config}";
string projectFile = CreateProjectWithNativeReference(id);
string nativeSource = @"
#include <stdio.h>
extern ""C"" {
int cpp_add(int a, int b) {
return a + b;
}
}";
File.WriteAllText(Path.Combine(_projectDir!, "mylib.cpp"), nativeSource);
string myDllImportCs = @$"
using System.Runtime.InteropServices;
namespace {id};
public static class MyDllImports
{{
[DllImport(""mylib"")]
public static extern int cpp_add(int a, int b);
}}";
File.WriteAllText(Path.Combine(_projectDir!, "Pages", "MyDllImport.cs"), myDllImportCs);
AddItemsPropertiesToProject(projectFile, extraItems: @"<NativeFileReference Include=""mylib.cpp"" />");
BlazorBuild(id, config, expectedFileType: NativeFilesType.Relinked);
CheckNativeFileLinked(forPublish: false);
BlazorPublish(id, config, expectedFileType: NativeFilesType.Relinked);
CheckNativeFileLinked(forPublish: true);
void CheckNativeFileLinked(bool forPublish)
{
// very crude way to check that the native file was linked in
// needed because we don't run the blazor app yet
string objBuildDir = Path.Combine(_projectDir!, "obj", config, "net6.0", "wasm", forPublish ? "for-publish" : "for-build");
string pinvokeTableHPath = Path.Combine(objBuildDir, "pinvoke-table.h");
Assert.True(File.Exists(pinvokeTableHPath), $"Could not find {pinvokeTableHPath}");
string pinvokeTableHContents = File.ReadAllText(pinvokeTableHPath);
string pattern = $"\"cpp_add\".*{id}";
Assert.True(Regex.IsMatch(pinvokeTableHContents, pattern),
$"Could not find {pattern} in {pinvokeTableHPath}");
}
}
private string CreateProjectWithNativeReference(string id)
{
CreateBlazorWasmTemplateProject(id);
......
......@@ -79,6 +79,7 @@ private CommandResult PublishForRequiresWorkloadTest(string config, string extra
[Theory]
[InlineData("Debug")]
[InlineData("Release")]
[ActiveIssue("https://github.com/dotnet/runtime/issues/59538")]
public void Net50Projects_NativeReference(string config)
=> BuildNet50Project(config, aot: false, expectError: true, @"<NativeFileReference Include=""native-lib.o"" />");
......@@ -92,6 +93,7 @@ public void Net50Projects_NativeReference(string config)
[Theory]
[MemberData(nameof(Net50TestData))]
[ActiveIssue("https://github.com/dotnet/runtime/issues/59538")]
public void Net50Projects_AOT(string config, bool aot, bool expectError)
=> BuildNet50Project(config, aot: aot, expectError: expectError);
......
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.IO;
using Xunit;
using Xunit.Abstractions;
......
......@@ -77,6 +77,7 @@ protected string Rebuild(bool nativeRelink, bool invariant, BuildArgs buildArgs,
buildArgs = newBuildArgs;
_testOutput.WriteLine($"{Environment.NewLine}Rebuilding with no changes ..{Environment.NewLine}");
Console.WriteLine($"{Environment.NewLine}Rebuilding with no changes ..{Environment.NewLine}");
(_, string output) = BuildProject(buildArgs,
id: id,
dotnetWasmFromRuntimePack: false,
......@@ -137,6 +138,18 @@ internal void CompareStat(IDictionary<string, FileStat> oldStat, IDictionary<str
throw new XunitException($"CompareStat failed:{Environment.NewLine}{msg}");
}
internal IDictionary<string, (string fullPath, bool unchanged)> GetFilesTable(bool unchanged, params string[] baseDirs)
{
var dict = new Dictionary<string, (string fullPath, bool unchanged)>();
foreach (var baseDir in baseDirs)
{
foreach (var file in Directory.EnumerateFiles(baseDir, "*", new EnumerationOptions { RecurseSubdirectories = true }))
dict[Path.GetFileName(file)] = (file, unchanged);
}
return dict;
}
internal IDictionary<string, (string fullPath, bool unchanged)> GetFilesTable(BuildArgs buildArgs, BuildPaths paths, bool unchanged)
{
List<string> files = new()
......
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.IO;
using System.Linq;
using Wasm.Build.Tests;
using Xunit;
......@@ -33,5 +34,60 @@ public void NoOpRebuildForNativeBuilds(BuildArgs buildArgs, bool nativeRelink, b
CompareStat(originalStat, newStat, pathsDict.Values);
RunAndTestWasmApp(buildArgs, buildDir: _projectDir, expectedExitCode: 42, host: host, id: id);
}
[ConditionalTheory(typeof(BuildTestBase), nameof(IsUsingWorkloads))]
[InlineData("Debug")]
[InlineData("Release")]
public void BlazorNoopRebuild(string config)
{
string id = $"blz_rebuild_{config}";
string projectFile = CreateBlazorWasmTemplateProject(id);
AddItemsPropertiesToProject(projectFile, extraProperties: "<WasmBuildNative>true</WasmBuildNative>");
string objDir = Path.Combine(_projectDir!, "obj", config, "net6.0", "wasm");
BlazorBuild(id, config, NativeFilesType.Relinked);
File.Move(Path.Combine(s_buildEnv.LogRootPath, id, $"{id}-build.binlog"),
Path.Combine(s_buildEnv.LogRootPath, id, $"{id}-build-first.binlog"));
var pathsDict = GetFilesTable(true, objDir);
pathsDict.Remove("runtime-icall-table.h");
var originalStat = StatFiles(pathsDict.Select(kvp => kvp.Value.fullPath));
// build again
BlazorBuild(id, config, NativeFilesType.Relinked);
var newStat = StatFiles(pathsDict.Select(kvp => kvp.Value.fullPath));
CompareStat(originalStat, newStat, pathsDict.Values);
}
[ConditionalTheory(typeof(BuildTestBase), nameof(IsUsingWorkloads))]
[InlineData("Debug")]
[InlineData("Release")]
public void BlazorOnlyLinkRebuild(string config)
{
string id = $"blz_relink_{config}";
string projectFile = CreateBlazorWasmTemplateProject(id);
AddItemsPropertiesToProject(projectFile, extraProperties: "<WasmBuildNative>true</WasmBuildNative>");
string objDir = Path.Combine(_projectDir!, "obj", config, "net6.0", "wasm");
BlazorBuild(id, config, NativeFilesType.Relinked);
File.Move(Path.Combine(s_buildEnv.LogRootPath, id, $"{id}-build.binlog"),
Path.Combine(s_buildEnv.LogRootPath, id, $"{id}-build-first.binlog"));
var pathsDict = GetFilesTable(true, objDir);
pathsDict.Remove("runtime-icall-table.h");
pathsDict.UpdateTo(unchanged: false, "dotnet.wasm", "dotnet.js", "emcc-link.rsp");
var originalStat = StatFiles(pathsDict.Select(kvp => kvp.Value.fullPath));
// build again
BlazorBuild(id, config, NativeFilesType.Relinked, "-p:EmccLinkOptimizationFlag=-O1");
var newStat = StatFiles(pathsDict.Select(kvp => kvp.Value.fullPath));
CompareStat(originalStat, newStat, pathsDict.Values);
}
}
}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.IO;
using Xunit;
using Xunit.Abstractions;
#nullable enable
namespace Wasm.Build.Tests
{
public class PInvokeTableGeneratorTests : BuildTestBase
{
public PInvokeTableGeneratorTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext)
: base(output, buildContext)
{
}
[ConditionalTheory(typeof(BuildTestBase), nameof(IsUsingWorkloads))]
[BuildAndRun(host: RunHost.V8)]
public void NativeLibraryWithVariadicFunctions(BuildArgs buildArgs, RunHost host, string id)
{
string code = @"
using System;
using System.Runtime.InteropServices;
public class Test
{
public static int Main(string[] args)
{
Console.WriteLine($""Main running"");
if (args.Length > 0)
{
// We don't want to run this, because we can't call variadic functions
Console.WriteLine($""sum_three: {sum_three(7, 14, 21)}"");
Console.WriteLine($""sum_two: {sum_two(3, 6)}"");
Console.WriteLine($""sum_one: {sum_one(5)}"");
}
return 42;
}
[DllImport(""variadic"", EntryPoint=""sum"")] public static extern int sum_one(int a);
[DllImport(""variadic"", EntryPoint=""sum"")] public static extern int sum_two(int a, int b);
[DllImport(""variadic"", EntryPoint=""sum"")] public static extern int sum_three(int a, int b, int c);
}";
(buildArgs, string output) = BuildForVariadicFunctionTests(code,
buildArgs with { ProjectName = $"variadic_{buildArgs.Config}_{id}" },
id);
Assert.Matches("warning.*native function.*sum.*varargs", output);
Assert.Matches("warning.*sum_(one|two|three)", output);
output = RunAndTestWasmApp(buildArgs, buildDir: _projectDir, expectedExitCode: 42, host: host, id: id);
Assert.Contains("Main running", output);
}
[ConditionalTheory(typeof(BuildTestBase), nameof(IsUsingWorkloads))]
[BuildAndRun(host: RunHost.V8)]
public void DllImportWithFunctionPointersCompilesWithWarning(BuildArgs buildArgs, RunHost host, string id)
{
string code = @"
using System;
using System.Runtime.InteropServices;
public class Test
{
public static int Main()
{
Console.WriteLine($""Main running"");
return 42;
}
[DllImport(""variadic"", EntryPoint=""sum"")]
public unsafe static extern int using_sum_one(delegate* unmanaged<char*, IntPtr, void> callback);
[DllImport(""variadic"", EntryPoint=""sum"")]
public static extern int sum_one(int a, int b);
}";
(buildArgs, string output) = BuildForVariadicFunctionTests(code,
buildArgs with { ProjectName = $"fnptr_{buildArgs.Config}_{id}" },
id);
Assert.Matches("warning.*Skipping.*because.*function pointer", output);
Assert.Matches("warning.*using_sum_one", output);
output = RunAndTestWasmApp(buildArgs, buildDir: _projectDir, expectedExitCode: 42, host: host, id: id);
Assert.Contains("Main running", output);
}
[ConditionalTheory(typeof(BuildTestBase), nameof(IsUsingWorkloads))]
[BuildAndRun(host: RunHost.V8)]
public void DllImportWithFunctionPointers_ForVariadicFunction_CompilesWithWarning(BuildArgs buildArgs, RunHost host, string id)
{
string code = @"
using System;
using System.Runtime.InteropServices;
public class Test
{
public static int Main()
{
Console.WriteLine($""Main running"");
return 42;
}
[DllImport(""variadic"", EntryPoint=""sum"")]
public unsafe static extern int using_sum_one(delegate* unmanaged<char*, IntPtr, void> callback);
}";
(buildArgs, string output) = BuildForVariadicFunctionTests(code,
buildArgs with { ProjectName = $"fnptr_variadic_{buildArgs.Config}_{id}" },
id);
Assert.Matches("warning.*Skipping.*because.*function pointer", output);
Assert.Matches("warning.*using_sum_one", output);
output = RunAndTestWasmApp(buildArgs, buildDir: _projectDir, expectedExitCode: 42, host: host, id: id);
Assert.Contains("Main running", output);
}
private (BuildArgs, string) BuildForVariadicFunctionTests(string programText, BuildArgs buildArgs, string id)
{
string filename = "variadic.o";
buildArgs = ExpandBuildArgs(buildArgs,
extraItems: $"<NativeFileReference Include=\"{filename}\" />",
extraProperties: "<AllowUnsafeBlocks>true</AllowUnsafeBlocks><_WasmDevel>true</_WasmDevel>");
(_, string output) = BuildProject(buildArgs,
initProject: () =>
{
File.WriteAllText(Path.Combine(_projectDir!, "Program.cs"), programText);
File.Copy(Path.Combine(BuildEnvironment.TestAssetsPath, "native-libs", filename),
Path.Combine(_projectDir!, filename));
},
publish: buildArgs.AOT,
id: id,
dotnetWasmFromRuntimePack: false);
return (buildArgs, output);
}
}
}
#include <stdarg.h>
int sum(int n, ...)
{
int result = 0;
va_list ptr;
va_start(ptr, n);
for (int i = 0; i < n; i++)
result += va_arg(ptr, int);
va_end(ptr);
return result;
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册