Skip to content
This repository has been archived by the owner on Nov 1, 2020. It is now read-only.

Automatic generation of method export file for shared libraries #5154

Merged
merged 23 commits into from
Jan 2, 2018

Conversation

tonerdo
Copy link
Contributor

@tonerdo tonerdo commented Dec 26, 2017

This PR adds automatic generation of .def, .exports and .map passed with linker options /DEF, exported_symbols_list and version-script for building shared libraries on Windows, macOS and Linux respectively.

Building shared libraries on all platforms is now a simple:

dotnet build /t:LinkNative /p:NativeLib=Shared

A good side effect of this change is that on non-Windows platforms it shaves off about 2MB from the produced binary because internally used NativeCallable methods are no longer exported by default

Fixes #4986


private string GetExportsFileExtenstion()
{
if (_targetDetails.OperatingSystem == TargetOS.Windows)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file name should be an argument on the command line.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't this be an extra cognitive load for the developer? I was thinking that just having everything done automatically might be better, experience wise

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant it should be argument on ilc.exe command and passed in by the msbuild script. The msbuild script is computing the name already and passing it to the linker, so it can pass it to ilc.exe as well.

Most developers won't invoke ilc.exe directly, so they won't really notice this argument.

if (!factory.NodeAliases.Values.Contains("__managed__Startup"))
return;

var nativeCallables = factory.NodeAliases.Where(n => n.Key is IMethodNode)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should get this list in ExportedMethodsRootProvider as it is rooting the native exports instead of trying to reverse engineer it here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will an internal static field on ExportedMethodsRootProvider suffice?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should not be static field. It should be instance field.

Maybe the function to produce the exports file can be moved to ExportedMethodsRootProvider - it does not need to be in separate type.

@tonerdo
Copy link
Contributor Author

tonerdo commented Dec 26, 2017

I now also pass exported_symbols_list on Linux, clang-3.9 doesn't support --version-script, and we do not use ld directly. That goes without saying that until #4988 is fixed this can't be tested on Linux.

@@ -70,7 +70,8 @@ See the LICENSE file in the project root for more information.
<LinkerArg Include="-luuid" Condition="'$(TargetOS)' != 'OSX'" />
<LinkerArg Include="-lrt" Condition="'$(TargetOS)' != 'OSX'" />
<LinkerArg Include="-licucore" Condition="'$(TargetOS)' == 'OSX'" />
<LinkerArg Include="-shared" Condition="'$(NativeLib)' == 'Shared'" />
<LinkerArg Include="-dynamiclib" Condition="'$(TargetOS)' == 'OSX' and '$(NativeLib)' == 'Shared'" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I though that -shared works on OSX too. Just for my education - why did you change this to -dynamiclib?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah I know it works. I guess I was just trying to be as platform accurate as possible, most Apple docs use -dynamiclib

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, that's fine.

@@ -151,7 +156,8 @@ See the LICENSE file in the project root for more information.
<IlcArg Condition="$(DebugSymbols) == 'true'" Include="-g" />
<IlcArg Condition="$(IlcGenerateMapFile) == 'true'" Include="--map:$(NativeIntermediateOutputPath)%(ManagedBinary.Filename).map.xml" />
<IlcArg Condition="$(RdXmlFile) != ''" Include="--rdxml:$(RdXmlFile)" />
<IlcArg Condition="$(OutputType) == 'Library' and $(NativeLib) != ''" Include="--nativelib" />
<IlcArg Condition="$(OutputType) == 'Library' and $(NativeLib) != ''" Include="--nativelib" />
<IlcArg Condition="$(OutputType) == 'Library' and $(NativeLib) == 'Shared'" Include="--exportsfile:$(ExportsFile)" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would change the condition to $(ExportsFile) != '' to make it simpler and more flexible

@@ -193,6 +199,8 @@ See the LICENSE file in the project root for more information.
<CustomLinkerArg Include="$(NativeObject)" />
<CustomLinkerArg Include="-o $(NativeBinary)" Condition="'$(OS)' != 'Windows_NT'" />
<CustomLinkerArg Include="/OUT:$(NativeBinary)" Condition="'$(OS)' == 'Windows_NT'" />
<CustomLinkerArg Include="/DEF:$(ExportsFile)" Condition="'$(OS)' == 'Windows_NT' and '$(NativeLib)' == 'Shared'" />
<CustomLinkerArg Include="-exported_symbols_list $(ExportsFile)" Condition="'$(TargetOS)' != 'Windows_NT' and '$(NativeLib)' == 'Shared'" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here


// Only emit exported methods for user supplied assemblies
// _exportsFile is null when we're not doing a shared library build
if (_module != _module.Context.SystemModule && _exportsFile != null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_module != _module.Context.SystemModule part of the condition should not be required (once the other part of the feedback is addressed).

@@ -356,7 +358,7 @@ private int Run(string[] args)
// TODO: Wasm fails to compile some of the xported methods due to missing opcodes
if (!_isWasmCodegen)
{
compilationRoots.Add(new ExportedMethodsRootProvider((EcmaModule)typeSystemContext.SystemModule));
compilationRoots.Add(new ExportedMethodsRootProvider((EcmaModule)typeSystemContext.SystemModule, _exportsFile));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not pass in _exportsFile argument here - pass in null, or make null to be default value of the argument.

// Only emit exported methods for user supplied assemblies
// _exportsFile is null when we're not doing a shared library build
if (_module != _module.Context.SystemModule && _exportsFile != null)
EmitExportedMethods();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MichalStrehovsky What do you think about this call here? Do you think it would be better to make it an explicit call from the compiler driver?

private void EmitExportedMethods()
{
string moduleName = Path.GetFileNameWithoutExtension(_exportsFile);
StringBuilder stringBuilder = new StringBuilder();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: It is better to write to the stream directly as you go instead of creating the whole string in memory and the writing it at once. (It does not matter a lot here since the file will be relatively small.)

            using (var writer = new StreamWriter(fileName))
            {
                writer.Write(...);
                writer.WriteLine(...);
            }

if (_module.Context.Target.IsWindows)
{
stringBuilder.Append("LIBRARY ");
stringBuilder.AppendLine(moduleName.ToUpper());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be ToUpperInvariant so that it does not depend on the current OS culture.


stringBuilder.AppendLine("EXPORTS");
foreach (var method in _methods)
stringBuilder.AppendLine(" " + method.GetNativeCallableExportName());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: You can consider using interpolated strings for all these - it looks better:

AppendLine($" {method.GetNativeCallableExportName()}");

<NativeObject>$(NativeIntermediateOutputPath)$(TargetName)$(NativeObjectExt)</NativeObject>
<NativeBinary>$(NativeOutputPath)$(TargetName)$(NativeBinaryExt)</NativeBinary>
<ExportsFile>$(NativeIntermediateOutputPath)$(TargetName)$(ExportsFiletExt)</ExportsFile>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only set this if it is empty and we are producing SharedLib. (It allows users to supply custom exports file.)

{
if (_module.Context.Target.IsWindows)
{
streamWriter.Write("LIBRARY ");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It the LIBRARY line actually needed? It seem unnecessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created it as specified in the DEF file documentation. I'll do a test and get back to you

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Number of the .def files in CoreCLR do not have the LIBRARY line. e.g. https://github.com/dotnet/coreclr/blob/master/src/dlls/dbgshim/dbgshim.ntdef

Copy link
Contributor Author

@tonerdo tonerdo Dec 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that's fine then

private void EmitExportedMethods()
{
string moduleName = Path.GetFileNameWithoutExtension(_exportsFile);
FileStream fileStream = new FileStream(_exportsFile, FileMode.OpenOrCreate);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be FileMode.Create. We want to overwrite the file if it exists already.

@@ -305,7 +307,7 @@ private int Run(string[] args)
// TODO: Wasm fails to compile some of the exported methods due to missing opcodes
if (!_isWasmCodegen)
{
compilationRoots.Add(new ExportedMethodsRootProvider(module));
compilationRoots.Add(new ExportedMethodsRootProvider(module, _exportsFile));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there are multiple assemblies specified in InputFilePaths that have NativeCallable methods in them, they're going to stomp over each other's writes into the exports file.

I would prefer a scheme where ExportedMethodsRootProvider has a public property that returns an IEnumerable<string> of all the exported symbols.

Once we're done adding to the compilationRoots list here, we pass the list of compilation roots to a helper method that does a foreach over the list of root provider, finds the list of exported methods from each ExportedMethodsRootProvider, and writes it to a file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah thanks for this, totally overlooked the multiple input file paths scenario. On what type do you suggest the helper method should be?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On what type do you suggest the helper method should be?

I would probably just add a DefFileWriter.cs or something like that to the ILCompiler project (it probably doesn't even need to be in the ILCompiler.Compiler project). I also wouldn't mind much if it was in Program.cs, but that file is starting to be a dumping ground for lots of short methods and maybe we should stop doing that.

@@ -302,7 +304,11 @@ private int Run(string[] args)
entrypointModule = module;
}

compilationRoots.Add(new ExportedMethodsRootProvider(module));
// TODO: Wasm fails to compile some of the exported methods due to missing opcodes
if (!_isWasmCodegen)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This coming back looks like a bad merge.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this. Been trying to fix the Windows build error

{
get
{
return _methods;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you factor this class in a way that we can't get different results depending on when ExportedMethods is accessed? It's not a problem right now, but I can see someone getting burned by this at some point.

if (nativeCallableExportName != null)
{
if (ecmaMethod.Module != _module.Context.SystemModule)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skipping exports from the system module is a policy decision. This class should ideally be policy-free (some other component might want this list too and a filtered list might not be what they intended). Could we filter in DefFileWriter.cs?


namespace ILCompiler
{
public class DefFileWriter
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is called ExportsFile everywhere else in this change. This should be called ExportsFileWriter to match.

DefFileWriter defFileWriter = new DefFileWriter(typeSystemContext, _exportsFile);
foreach (var compilationRoot in compilationRoots)
{
if (compilationRoot is ExportedMethodsRootProvider)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: You can use new C# features to avoid the cast.

 if (compilationRoot is ExportedMethodsRootProvider provider)
     defFileWriter.AddExportedMethod(provider);

@@ -446,6 +456,17 @@ private int Run(string[] args)
ObjectDumper dumper = _mapFileName != null ? new ObjectDumper(_mapFileName) : null;

CompilationResults compilationResults = compilation.Compile(_outputFilePath, dumper);
if (_nativeLib)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition should be _exportsFile != null.

@tonerdo
Copy link
Contributor Author

tonerdo commented Dec 31, 2017

@jkotas @MichalStrehovsky I can't for the life of me figure out why my latest change causes the WASM tests to fail. It's just a refactoring that doesn't change the end result of the AddCompilationRoots method.

if (_methods != null)
return _methods;

_methods = new List<EcmaMethod>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: It is a better pattern to create the list in a local, and only assign it to the field once it is done. (Slightly better performance and reliability.)

}
}

foreach (var ecmaMethod in ExportedMethods)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think we need to be differentiating between NativeCallable and RuntimeExports here. They can be treated the same.

@jkotas
Copy link
Member

jkotas commented Dec 31, 2017

I can't for the life of me figure out why my latest change causes the WASM tests to fail.

I do not see anything obvious either. I have kicked a fresh CI run on #5171 to see whether it is a problem that is affecting all PRs.

@tonerdo
Copy link
Contributor Author

tonerdo commented Dec 31, 2017

whether it is a problem that is affecting all PRs.

The issue only happens with my inclusion of the ExportedMethods property, it passes fine when I revert it to how it was. It's just really weird

foreach (var method in type.GetMethods())
{
EcmaMethod ecmaMethod = (EcmaMethod)method;
var methods = new List<EcmaMethod>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Building the List of methods made sense when it was cached. Without cache, it would be better to yield return the methods.

Either keep the cache, or replace List with yield return.

@tonerdo
Copy link
Contributor Author

tonerdo commented Dec 31, 2017

I think the WASM tests failed because there were two different loops for RuntimeExport and NativeCallable, don't know why but it works fine when a single loop is used instead.

@tonerdo
Copy link
Contributor Author

tonerdo commented Jan 2, 2018

Anything else required of me on this?

@jkotas
Copy link
Member

jkotas commented Jan 2, 2018

@MichalStrehovsky Does this look good to you?

Copy link
Member

@MichalStrehovsky MichalStrehovsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good minus the nit.

@@ -48,8 +49,12 @@ See the LICENSE file in the project root for more information.
<NativeBinaryExt Condition="'$(OutputType)' != 'Exe' and '$(TargetOS)' != 'Windows_NT' and $(NativeLib) == 'Static'">.a</NativeBinaryExt>
<NativeBinaryExt Condition="'$(NativeCodeGen)' == 'wasm'">.html</NativeBinaryExt>

<ExportsFiletExt Condition="'$(OutputType)' != 'Exe' and '$(TargetOS)' == 'Windows_NT' and '$(NativeLib)' == 'Shared'">.def</ExportsFiletExt>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: typo: Filet -> File

@jkotas jkotas merged commit d5a0166 into dotnet:master Jan 2, 2018
@jkotas
Copy link
Member

jkotas commented Jan 2, 2018

@tonerdo Thank you!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants