diff --git a/Agents/Xamarin.Interactive.Mac/NativeExceptionHandler.cs b/Agents/Xamarin.Interactive.Mac/NativeExceptionHandler.cs index 901011a0a..2dccf303b 100644 --- a/Agents/Xamarin.Interactive.Mac/NativeExceptionHandler.cs +++ b/Agents/Xamarin.Interactive.Mac/NativeExceptionHandler.cs @@ -47,7 +47,7 @@ static readonly IntPtr objCExceptionPreprocessorFnptr static IntPtr ObjCExceptionPreprocessor (IntPtr exceptionPtr) { - throw new TrappedNativeException (Runtime.GetNSObject (exceptionPtr)); + throw new TrappedNativeException (ObjCRuntime.Runtime.GetNSObject (exceptionPtr)); } public static IDisposable Trap () diff --git a/Agents/Xamarin.Interactive.iOS/NativeExtensions.cs b/Agents/Xamarin.Interactive.iOS/NativeExtensions.cs index 0858d93a2..1c8c3f7d0 100644 --- a/Agents/Xamarin.Interactive.iOS/NativeExtensions.cs +++ b/Agents/Xamarin.Interactive.iOS/NativeExtensions.cs @@ -40,7 +40,7 @@ public static UIWindow GetStatusBarWindow (this UIApplication app) return null; var ptr = IntPtr_objc_msgSend (app.Handle, Selectors.statusBarWindow.Handle); - return ptr != IntPtr.Zero ? (UIWindow)Runtime.GetNSObject (ptr) : null; + return ptr != IntPtr.Zero ? (UIWindow)ObjCRuntime.Runtime.GetNSObject (ptr) : null; } public static void TryHideStatusClockView (this UIApplication app) diff --git a/Agents/Xamarin.Interactive/CodeAnalysis/Evaluating/EvaluationContextManager.cs b/Agents/Xamarin.Interactive/CodeAnalysis/Evaluating/EvaluationContextManager.cs index 7392cad88..ec00aa4a5 100644 --- a/Agents/Xamarin.Interactive/CodeAnalysis/Evaluating/EvaluationContextManager.cs +++ b/Agents/Xamarin.Interactive/CodeAnalysis/Evaluating/EvaluationContextManager.cs @@ -113,6 +113,7 @@ public Task CreateEvaluationContextAsync ( var globalStateObject = CreateGlobalState (); targetCompilationConfiguration = targetCompilationConfiguration.With ( + runtime: Runtime.CurrentProcessRuntime, defaultImports: defaultImports); targetCompilationConfiguration = PrepareTargetCompilationConfiguration ( diff --git a/Agents/Xamarin.Interactive/CodeAnalysis/Resolving/DllMap.cs b/Agents/Xamarin.Interactive/CodeAnalysis/Resolving/DllMap.cs new file mode 100644 index 000000000..44a97b3cc --- /dev/null +++ b/Agents/Xamarin.Interactive/CodeAnalysis/Resolving/DllMap.cs @@ -0,0 +1,242 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Xml; + +namespace Xamarin.Interactive.CodeAnalysis.Resolving +{ + sealed class DllMap : IEnumerable> + { + public struct Entity : IEquatable + { + public string LibraryName { get; } + public string SymbolName { get; } + + public Entity ( + string libraryName, + string symbolName = null) + { + LibraryName = libraryName; + SymbolName = symbolName; + } + + public bool Equals (Entity other) + => LibraryName == other.LibraryName && SymbolName == other.SymbolName; + + public override bool Equals (object obj) + => obj is Entity entity && Equals (entity); + + public override int GetHashCode () + => Hash.Combine (LibraryName, SymbolName); + + public override string ToString () + => $"({LibraryName ?? "(null)"}, {SymbolName ?? "(null)"})"; + } + + internal struct Filter + { + public string OperatingSystem { get; } + public string Cpu { get; } + public string WordSize { get; } + + public Filter ( + string operatingSystem, + string cpu, + string wordSize) + { + OperatingSystem = operatingSystem; + Cpu = cpu; + WordSize = wordSize; + } + + public bool Matches (Filter targetFilter) + { + if (OperatingSystem != null && + targetFilter.OperatingSystem != null && + !Matches (targetFilter.OperatingSystem, OperatingSystem)) + return false; + + if (Cpu != null && + targetFilter.Cpu != null && + !Matches (targetFilter.Cpu, Cpu)) + return false; + + if (WordSize != null && + targetFilter.WordSize != null && + !Matches (targetFilter.WordSize, WordSize)) + return false; + + return true; + } + + static bool Matches (string value, string predicate) + { + if (string.IsNullOrEmpty (predicate) || string.IsNullOrEmpty (value)) + return false; + + var invert = false; + + if (predicate [0] == '!') { + invert = true; + predicate = predicate.Substring (1); + } + + foreach (var predicateItem in predicate.Split (',')) { + if (predicateItem == value) + return !invert; + } + + return invert; + } + } + + readonly Filter targetFilter; + readonly Dictionary map = new Dictionary (); + + public DllMap () : this (Runtime.CurrentProcessRuntime) + { + } + + public DllMap (Runtime targetRuntime) + { + var os = targetRuntime.OSPlatform.ToString ().ToLowerInvariant (); + + string cpu = null; + string wordSize = null; + switch (targetRuntime.Architecture) { + case Architecture.X86: + cpu = "x86"; + wordSize = "32"; + break; + case Architecture.X64: + cpu = "x86-64"; + wordSize = "64"; + break; + case Architecture.Arm: + cpu = "arm"; + wordSize = "32"; + break; + case Architecture.Arm64: + cpu = "armv8"; + wordSize = "64"; + break; + } + + targetFilter = new Filter (os, cpu, wordSize); + } + + public bool TryMap (Entity source, out Entity target) + { + if (!map.TryGetValue (source, out target) && + !map.TryGetValue (new Entity (source.LibraryName), out target)) { + target = source; + return false; + } + + target = new Entity (target.LibraryName, target.SymbolName ?? source.SymbolName); + return true; + } + + public bool TryMap (Entity source, string basePath, out Entity target) + { + if (basePath == null) + throw new ArgumentNullException (nameof (basePath)); + + if (TryMap (source, out target)) { + target = new Entity ( + Path.Combine (basePath, target.LibraryName), + target.SymbolName); + return true; + } + + return false; + } + + public DllMap Add (Entity source, Entity target) + { + map.Add (source, target); + return this; + } + + public DllMap LoadXml (string configurationXml) + { + var document = new XmlDocument (); + document.LoadXml (configurationXml); + return Load (document); + } + + public DllMap Load (string configurationFile) + { + var document = new XmlDocument (); + document.Load (configurationFile); + return Load (document); + } + + public DllMap Load (XmlDocument document) + { + if (document == null) + throw new ArgumentNullException (nameof (document)); + + if (document.DocumentElement == null) + throw new ArgumentException ( + "document is not loaded or has no root element", + nameof (document)); + + if (document.DocumentElement.Name != "configuration") + return this; + + string GetAttribute (XmlElement elem, string attributeName) + { + var value = elem.GetAttribute (attributeName); + return string.IsNullOrEmpty (value) ? null : value; + } + + Filter CreateFilter (XmlElement elem) + => new Filter ( + GetAttribute (elem, "os"), + GetAttribute (elem, "cpu"), + GetAttribute (elem, "wordsize")); + + foreach (var node in document.DocumentElement.ChildNodes) { + if (node is XmlElement elem && elem.Name == "dllmap") { + if (!CreateFilter (elem).Matches (targetFilter)) + continue; + + var sourceLibrary = GetAttribute (elem, "dll"); + var targetLibrary = GetAttribute (elem, "target"); + + foreach (var childNode in elem.ChildNodes) { + if (childNode is XmlElement childElem && childElem.Name == "dllentry") { + if (!CreateFilter (childElem).Matches (targetFilter)) + continue; + + var symbolTargetLibrary = GetAttribute (childElem, "dll") ?? targetLibrary; + var sourceSymbol = GetAttribute (childElem, "name"); + var targetSymbol = GetAttribute (childElem, "target"); + + if (symbolTargetLibrary != null) + map [new Entity (sourceLibrary, sourceSymbol)] + = new Entity (symbolTargetLibrary, targetSymbol); + } + } + + if (!elem.HasChildNodes && targetLibrary != null) + map [new Entity (sourceLibrary)] = new Entity (targetLibrary); + } + } + + return this; + } + + public IEnumerator> GetEnumerator () + => map.GetEnumerator (); + + IEnumerator IEnumerable.GetEnumerator () + => GetEnumerator (); + } +} \ No newline at end of file diff --git a/Agents/Xamarin.Interactive/CodeAnalysis/TargetCompilationConfiguration.cs b/Agents/Xamarin.Interactive/CodeAnalysis/TargetCompilationConfiguration.cs index dbe673d04..0c75b2498 100644 --- a/Agents/Xamarin.Interactive/CodeAnalysis/TargetCompilationConfiguration.cs +++ b/Agents/Xamarin.Interactive/CodeAnalysis/TargetCompilationConfiguration.cs @@ -17,6 +17,7 @@ public sealed class TargetCompilationConfiguration public static TargetCompilationConfiguration CreateInitialForCompilationWorkspace ( IReadOnlyList assemblySearchPaths = null) => new TargetCompilationConfiguration ( + default, default, HostEnvironment.OS, default, @@ -27,6 +28,7 @@ public static TargetCompilationConfiguration CreateInitialForCompilationWorkspac new List (assemblySearchPaths ?? Array.Empty ()), default); + public Runtime Runtime { get; } public Sdk Sdk { get; } internal HostOS CompilationOS { get; } public EvaluationContextId EvaluationContextId { get; } @@ -39,6 +41,7 @@ public static TargetCompilationConfiguration CreateInitialForCompilationWorkspac [JsonConstructor] TargetCompilationConfiguration ( + Runtime runtime, Sdk sdk, HostOS compilationOS, EvaluationContextId evaluationContextId, @@ -49,6 +52,7 @@ public static TargetCompilationConfiguration CreateInitialForCompilationWorkspac IReadOnlyList assemblySearchPaths, bool includePEImagesInDependencyResolution) { + Runtime = runtime; Sdk = sdk; CompilationOS = compilationOS; EvaluationContextId = evaluationContextId; @@ -61,6 +65,7 @@ public static TargetCompilationConfiguration CreateInitialForCompilationWorkspac } internal TargetCompilationConfiguration With ( + Optional runtime = default, Optional sdk = default, Optional compilationOS = default, Optional evaluationContextId = default, @@ -71,6 +76,7 @@ internal TargetCompilationConfiguration With ( Optional> assemblySearchPaths = default, Optional includePEImagesInDependencyResolution = default) => new TargetCompilationConfiguration ( + runtime.GetValueOrDefault (Runtime), sdk.GetValueOrDefault (Sdk), compilationOS.GetValueOrDefault (CompilationOS), evaluationContextId.GetValueOrDefault (EvaluationContextId), diff --git a/Agents/Xamarin.Interactive/Runtime.cs b/Agents/Xamarin.Interactive/Runtime.cs new file mode 100644 index 000000000..a86031a39 --- /dev/null +++ b/Agents/Xamarin.Interactive/Runtime.cs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Runtime.InteropServices; + +using Newtonsoft.Json; + +namespace Xamarin.Interactive +{ + using static Architecture; + + [JsonObject] + public struct Runtime : IEquatable + { + static OSPlatform GetOSPlatform () + { + if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + return OSPlatform.Windows; + else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) + return OSPlatform.OSX; + else if (RuntimeInformation.IsOSPlatform (OSPlatform.Linux)) + return OSPlatform.Linux; + return default (OSPlatform); + } + + public static Runtime CurrentProcessRuntime { get; } = new Runtime ( + GetOSPlatform (), + RuntimeInformation.ProcessArchitecture, + null); + + public OSPlatform OSPlatform { get; } + public Architecture? Architecture { get; } + public string RuntimeIdentifier { get; } + + [JsonConstructor] + public Runtime ( + OSPlatform osPlatform, + Architecture? architecture = null, + string runtimeIdentifier = null) + { + OSPlatform = osPlatform; + Architecture = architecture; + + RuntimeIdentifier = runtimeIdentifier; + if (RuntimeIdentifier == null) + RuntimeIdentifier = BuildRuntimeIdentifier (); + } + + public Runtime WithRuntimeIdentifier (string runtimeIdentifier) + => new Runtime ( + OSPlatform, + Architecture, + runtimeIdentifier); + + public bool Equals (Runtime other) + => other.OSPlatform == OSPlatform && + other.Architecture == Architecture && + other.RuntimeIdentifier == RuntimeIdentifier; + + public override bool Equals (object obj) + => obj is Runtime runtime && Equals (runtime); + + public override int GetHashCode () + => Hash.Combine ( + OSPlatform.GetHashCode (), + Architecture == null ? 0 : Architecture.GetHashCode (), + RuntimeIdentifier == null ? 0 : RuntimeIdentifier.GetHashCode ()); + + public override string ToString () + => RuntimeIdentifier; + + string BuildRuntimeIdentifier () + { + string rid; + + if (OSPlatform == OSPlatform.Windows) + rid = "win"; + else if (OSPlatform == OSPlatform.OSX) + rid = "osx"; + else if (OSPlatform == OSPlatform.Linux) + rid = "linux"; + else + rid = OSPlatform.ToString ().ToLowerInvariant (); + + if (Architecture == null) + return rid; + + switch (Architecture.Value) { + case X86: + rid += "-x86"; + break; + case X64: + rid += "-x64"; + break; + case Arm: + rid += "-arm"; + break; + case Arm64: + rid += "-arm64"; + break; + default: + rid += "-" + Architecture.Value.ToString ().ToLowerInvariant (); + break; + } + + return rid; + } + } +} \ No newline at end of file diff --git a/Agents/Xamarin.Interactive/Serialization/InteractiveJsonSerializerSettings.cs b/Agents/Xamarin.Interactive/Serialization/InteractiveJsonSerializerSettings.cs index 4a09e2014..c1bae3197 100644 --- a/Agents/Xamarin.Interactive/Serialization/InteractiveJsonSerializerSettings.cs +++ b/Agents/Xamarin.Interactive/Serialization/InteractiveJsonSerializerSettings.cs @@ -6,6 +6,7 @@ using System.Globalization; using System.Linq; using System.Reflection; +using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Runtime.Versioning; @@ -122,6 +123,7 @@ public Type BindToType (string assemblyName, string typeName) Converters.Add (new CodeCellIdConverter ()); Converters.Add (new EvaluationContextIdConverter ()); Converters.Add (new SdkIdConverter ()); + Converters.Add (new OSPlatformConverter ()); Converters.Add (new FrameworkNameConverter ()); Converters.Add (new IRepresentedTypeConverter ()); @@ -161,6 +163,13 @@ sealed class SdkIdConverter : StringConverter protected override SdkId GetValue (string value) => (SdkId)value; protected override string GetString (SdkId value) => (string)value; } + + sealed class OSPlatformConverter : StringConverter + { + protected override OSPlatform GetValue (string value) => OSPlatform.Create (value.ToUpperInvariant ()); + protected override string GetString (OSPlatform value) => value.ToString (); + } + sealed class FrameworkNameConverter : StringConverter { protected override FrameworkName GetValue (string value) => new FrameworkName (value); diff --git a/CodeAnalysis/Xamarin.Interactive.CodeAnalysis.Tests/DllMapTests.cs b/CodeAnalysis/Xamarin.Interactive.CodeAnalysis.Tests/DllMapTests.cs new file mode 100644 index 000000000..e84e86569 --- /dev/null +++ b/CodeAnalysis/Xamarin.Interactive.CodeAnalysis.Tests/DllMapTests.cs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Runtime.InteropServices; + +using Xunit; + +namespace Xamarin.Interactive.CodeAnalysis.Resolving +{ + public class DllMapTests + { + static DllMap.Filter ParseFilter (string filter) + { + string NullIfEmpty (string str) + => string.IsNullOrEmpty (str) ? null : str; + + var parts = filter.Split (';'); + return new DllMap.Filter ( + NullIfEmpty (parts [0]), + parts.Length > 1 ? NullIfEmpty (parts [1]) : null, + parts.Length > 2 ? NullIfEmpty (parts [2]) : null); + } + + [Theory] + [InlineData ("linux", "linux", true)] + [InlineData ("linux,osx", "linux", true)] + [InlineData ("linux,osx", "osx", true)] + [InlineData ("!linux", "windows", true)] + [InlineData ("!linux", "linux", false)] + [InlineData ("!linux,osx", "osx", false)] + [InlineData ("!linux,osx", "linux", false)] + [InlineData ("!linux,osx", "windows", true)] + [InlineData ("!linux,osx;!x86-64", "windows;x86", true)] + [InlineData ("!linux,osx;x86-64", "windows;x86", false)] + [InlineData ("!linux,osx;x86-64", "windows;x86-64", true)] + [InlineData ("!linux,osx;x86-64,arm", "windows;arm", true)] + [InlineData ("!linux,osx;!x86-64,arm", "windows;arm", false)] + [InlineData ("!linux,osx;!x86-64,arm", "windows;armv8", true)] + [InlineData ("windows;;32", "windows;;32", true)] + [InlineData ("windows;;!32", "windows;;32", false)] + [InlineData ("windows;;!64", "windows;;32", true)] + public void FilterMatch (string predicate, string host, bool expectedMatch) + => Assert.Equal ( + expectedMatch, + ParseFilter (predicate).Matches (ParseFilter (host))); + + [Theory] + [InlineData ("liba", null, "liba-mapped", null)] + [InlineData ("liba", "specialfunc", "libspecialfunc", "specialfunc")] + [InlineData ("liba", "specialfunc2", "libspecialfunc", "newspecialfunc2")] + public void CoreMaps ( + string sourceLibrary, + string sourceSymbol, + string targetLibrary, + string targetSymbol) + { + var map = new DllMap { + { + new DllMap.Entity ("liba"), + new DllMap.Entity ("liba-mapped") + }, + { + new DllMap.Entity ("liba", "specialfunc"), + new DllMap.Entity ("libspecialfunc") + }, + { + new DllMap.Entity ("liba", "specialfunc2"), + new DllMap.Entity ("libspecialfunc", "newspecialfunc2") + }, + }; + + var source = new DllMap.Entity (sourceLibrary, sourceSymbol); + var expectedTarget = new DllMap.Entity (targetLibrary, targetSymbol); + + Assert.True (map.TryMap (source, out var target)); + Assert.Equal (expectedTarget, target); + } + + const string dllmapXml = @" + + + + + + + + + + + + "; + + [Theory] + [InlineData ("windows", "libc", "somefunction", "libdifferent.so", "differentfunction", true)] + [InlineData ("solaris", "libc", "somefunction", "libanother.so", "differentfunction", true)] + [InlineData ("freebsd", "libc", "somefunction", "libanother.so", "differentfunction", true)] + [InlineData ("linux", "libc", "anyotherfunction", "preload-libc", "anyotherfunction", true)] + [InlineData ("osx", "libc", null, "preload-libc", null, true)] + [InlineData ("windows", "SolarSystem", "get_Animals", "SolarSystem", "get_Animals", false)] + [InlineData ("windows", "SolarSystem", "get_Plants", "SolarSystem", "get_Plants", false)] + [InlineData ("linux", "SolarSystem", "get_Animals", "libearth.so", "get_Animals", true)] + [InlineData ("linux", "SolarSystem", "get_Plants", "libmars.so", "get_Plants", true)] + public void XmlMap ( + string host, + string sourceLibrary, + string sourceSymbol, + string targetLibrary, + string targetSymbol, + bool expectedMatch) + { + var map = new DllMap (new Runtime (OSPlatform.Create (host))).LoadXml (dllmapXml); + + var source = new DllMap.Entity (sourceLibrary, sourceSymbol); + var expectedTarget = new DllMap.Entity (targetLibrary, targetSymbol); + + Assert.Equal (expectedMatch, map.TryMap (source, out var target)); + Assert.Equal (expectedTarget, target); + } + } +} \ No newline at end of file diff --git a/docs/Xamarin.Interactive.api.cs b/docs/Xamarin.Interactive.api.cs index 3b082142a..6a1e66c2c 100644 --- a/docs/Xamarin.Interactive.api.cs +++ b/docs/Xamarin.Interactive.api.cs @@ -76,6 +76,38 @@ public interface IAgentSynchronizationContext SynchronizationContext PushContext (SynchronizationContext context); } [JsonObject] + public struct Runtime : IEquatable + { + public Architecture? Architecture { + get; + } + + public static Runtime CurrentProcessRuntime { + get; + } + + public OSPlatform OSPlatform { + get; + } + + public string RuntimeIdentifier { + get; + } + + [JsonConstructor] + public Runtime (OSPlatform osPlatform, Architecture? architecture = null, string runtimeIdentifier = null); + + public bool Equals (Runtime other); + + public override bool Equals (object obj); + + public override int GetHashCode (); + + public override string ToString (); + + public Runtime WithRuntimeIdentifier (string runtimeIdentifier); + } + [JsonObject] public sealed class Sdk { public IReadOnlyList AssemblySearchPaths { @@ -277,6 +309,10 @@ public IReadOnlyList InitialReferences { get; } + public Runtime Runtime { + get; + } + public Sdk Sdk { get; }