Skip to content

Commit

Permalink
Merge pull request #50 from RobSwDev/RemoveEmbeddedNativeDlls_Designe…
Browse files Browse the repository at this point in the history
…rFix

Remove Embedded Native Dlls from managed Scintilla dll.
  • Loading branch information
Petteri Kautonen authored Jan 20, 2023
2 parents 5f52949 + 9bb3bca commit a54befc
Show file tree
Hide file tree
Showing 12 changed files with 66 additions and 208 deletions.
8 changes: 0 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,6 @@ One of the issues that ScintillaNET has historically suffered from is the fact t

No more. **One of the major focuses of this rewrite was to give ScintillaNET an understanding of Unicode from the ground up.** Every API now consistently works with character-based offsets and ranges just like .NET developers expect. Internally we maintain a mapping of character to byte offsets (and vice versa) and do all the translation for you so you never need to worry about it. No more out-of-range exceptions. No more confusion. No more pain. It just works.

### One Library

The second most popular ScintillaNET issue was confusion distributing the ScintillaNET DLL and its native component, the SciLexer DLL. ScintillaNET is a wrapper. Without the SciLexer.dll containing the core Scintilla functionality it is nothing. As a native component, SciLexer.dll has to be compiled separately for 32 and 64-bit versions of Windows. So it was actually three DLLs that developers had to ship with their applications.

This proved a pain point because developers often didn't want to distribute so many libraries or wanted to place them in alternate locations which would break the DLL loading mechanisms used by PInvoke and ScintillaNET. It also causes headaches during design-time in Visual Studio for the same reasons.

To address this ScintillaNET now embeds a 32 and 64-bit version of SciLexer.dll in the ScintillaNET DLL. **Everything you need to run ScintillaNET in one library.** In addition to soothing the pain mentioned above this now makes it possible for us to create a ScintillaNET NuGet package.

### Keeping it Consistent

Another goal of the rewrite was to accept the original Scintilla API for what it is and not try to coerce it into a .NET-style API when it should not or could not be. A good example of this is how ScintillaNET uses indexers to access lines, but not treat them as a .NET collection. Lines in a Scintilla control are not items in a collection. There is no API to Add, Insert, or Remove a line in Scintilla and thus we don't try to create one in ScintillaNET. These deviations from .NET convention are rare, but are done to keep any native Scintilla documentation relevant to the managed wrapper and to avoid situations where trying to force the original API into a more familiar one is more detrimental than helpful.
Expand Down
1 change: 1 addition & 0 deletions Scintilla.NET.sln
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{BE874649-2B00-415E-8C80-82A42D949696}"
ProjectSection(SolutionItems) = preProject
.github\workflows\dotnet-desktop.yml = .github\workflows\dotnet-desktop.yml
README.md = README.md
EndProjectSection
EndProject
Global
Expand Down
16 changes: 0 additions & 16 deletions Scintilla.NET/Lexilla.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,18 +78,6 @@ internal Lexilla(IntPtr lexillaHandle)

#endregion

#region PrivateMethods

private static void VerifyInitialized()
{
if (!initialized)
{
_ = Scintilla.GetModulePath();
}
}

#endregion

#region DllCalls
private static NativeMethods.GetLexerCount getLexerCount;

Expand All @@ -108,7 +96,6 @@ private static void VerifyInitialized()
/// <returns>Amount of lexers defined in the Lexilla library.</returns>
public static int GetLexerCount()
{
VerifyInitialized();
return (int) getLexerCount();
}

Expand All @@ -119,7 +106,6 @@ public static int GetLexerCount()
/// <returns>A <see cref="IntPtr"/> containing the lexer interface pointer.</returns>
public static IntPtr CreateLexer(string lexerName)
{
VerifyInitialized();
return createLexer(lexerName);
}

Expand All @@ -130,7 +116,6 @@ public static IntPtr CreateLexer(string lexerName)
/// <returns>The name of the lexer if one was found with the specified index; <c>null</c> otherwise.</returns>
public static string GetLexerName(int index)
{
VerifyInitialized();
var pointer = Marshal.AllocHGlobal(1024);
try
{
Expand All @@ -150,7 +135,6 @@ public static string GetLexerName(int index)
/// <returns>The name of the lexer if one was found with the specified identifier; <c>null</c> otherwise.</returns>
public static string LexerNameFromId(int identifier)
{
VerifyInitialized();
return lexerNameFromId(new IntPtr(identifier));
}

Expand Down
12 changes: 7 additions & 5 deletions Scintilla.NET/Scintilla.NET.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<NeutralLanguage>en-US</NeutralLanguage>
<Description>Source Editing Component based on Scintilla 5 series.</Description>
<Copyright>Copyright (c) 2018, Jacob Slusser. All rights reserved. VPKSoft, cyber960 2022.</Copyright>
<Version>5.3.2.3</Version>
<Version>5.3.2.4</Version>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DocumentationFile>bin\$(Configuration)\ScintillaNET.xml</DocumentationFile>
<UseWindowsForms>true</UseWindowsForms>
Expand Down Expand Up @@ -60,8 +60,6 @@
<Pack>True</Pack>
<PackagePath></PackagePath>
</None>
<EmbeddedResource Include="..\Shared\x64\Scintilla.zip" Link="x64\Scintilla.zip" />
<EmbeddedResource Include="..\Shared\x86\Scintilla.zip" Link="x86\Scintilla.zip" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\Shared\Annotation.cs" Link="Annotation.cs" />
Expand Down Expand Up @@ -160,7 +158,11 @@
</Compile>
</ItemGroup>
<ItemGroup>
<Folder Include="x64\" />
<Folder Include="x86\" />
<!-- Native dlls and nuget support -->
<None Include="..\native\**\*.*">
<Pack>True</Pack>
<PackagePath>build</PackagePath>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
224 changes: 45 additions & 179 deletions Shared/Scintilla.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,10 @@
using System.Drawing.Design;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Text;
using System.Threading;
using System.Windows.Forms;

namespace ScintillaNET
Expand All @@ -23,17 +19,55 @@ namespace ScintillaNET
[Docking(DockingBehavior.Ask)]
public class Scintilla : Control
{
static Scintilla()
{
string basePath = LocateNativeDllDirectory();
modulePathScintilla = Path.Combine(basePath, "Scintilla.dll");
#if SCINTILLA5
modulePathLexilla = Path.Combine(basePath, "Lexilla.dll");

try
{
var info = FileVersionInfo.GetVersionInfo(modulePathScintilla);
scintillaVersion = info.ProductVersion ?? info.FileVersion;
info = FileVersionInfo.GetVersionInfo(modulePathLexilla);
lexillaVersion = info.ProductVersion ?? info.FileVersion;
}
catch
{
scintillaVersion = "ERROR";
lexillaVersion = "ERROR";
}
#endif
}

private static string LocateNativeDllDirectory()
{
string platform = (IntPtr.Size == 4 ? "x86" : "x64");
string managedLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
var basePath = Path.Combine(managedLocation, platform);

// Directory exists when application is built, but not if Scintilla is dragged on in the Winforms designer
if (Directory.Exists(basePath))
{
return basePath;
}

// In the Designer, look for the native dlls in the nuget package
return Path.Combine(managedLocation, "..", "..", "build", platform);
}

#region Fields

// WM_DESTROY workaround
private static bool? reparentAll;
private bool reparent;

// Static module data
private static string modulePathScintilla;
private static readonly string modulePathScintilla;

#if SCINTILLA5
private static string modulePathLexilla;
private static readonly string modulePathLexilla;
#endif

private static IntPtr moduleHandle;
Expand Down Expand Up @@ -996,158 +1030,9 @@ public int GetEndStyled()
return Lines.ByteToCharPosition(pos);
}

internal static string GetModulePath()
{
// UI thread...
if (modulePathScintilla == null)
{
// Extract the embedded SciLexer DLL
// http://stackoverflow.com/a/768429/2073621
var version = typeof(Scintilla).Assembly.GetName().Version.ToString(3);

#if SCINTILLA5
var scintillaName = "Scintilla.NET";
var scintillaBaseName = "Scintilla.NET";

modulePathScintilla =
Path.Combine(
Path.Combine(Path.Combine(Path.Combine(Path.GetTempPath(), scintillaName), version),
(IntPtr.Size == 4 ? "x86" : "x64")), "Scintilla.dll");
modulePathLexilla =
Path.Combine(
Path.Combine(Path.Combine(Path.Combine(Path.GetTempPath(), scintillaName), version),
(IntPtr.Size == 4 ? "x86" : "x64")), "Lexilla.dll");
#elif SCINTILLA4
var scintillaName = "ScintillaNET";

modulePathScintilla =
Path.Combine(Path.Combine(Path.Combine(Path.Combine(Path.GetTempPath(), scintillaName), version), (IntPtr.Size == 4 ? "x86" : "x64")), "Scintilla.dll");
#endif



if (!File.Exists(modulePathScintilla))
{
// http://stackoverflow.com/a/229567/2073621
// Synchronize access to the file across processes

var assembly = Assembly.GetAssembly(typeof(Scintilla));

var guid = assembly?.FullName;

#if !NETCOREAPP
guid =
((GuidAttribute)assembly.GetCustomAttributes(typeof(GuidAttribute), false).GetValue(0)).Value.ToString();
#endif

var name = string.Format(CultureInfo.InvariantCulture, "Global\\{{{0}}}", guid);
using (var mutex = new Mutex(false, name))
{
// Blocked because the library version conflicted in the designer using .NET 7 and DevExpress WinForms,
// See: https://github.com/VPKSoft/ScintillaNET/issues/28
// See: https://supportcenter.devexpress.com/ticket/details/t1133409/scintilla-net-5-3-1-3-don-t-work-when-devexpress-libraries-added-to-project
#if USE_MUTEX_ACCESS
var access = new MutexAccessRule(new SecurityIdentifier(WellKnownSidType.WorldSid, null),
MutexRights.FullControl, AccessControlType.Allow);
var security = new MutexSecurity();
security.AddAccessRule(access);
mutex.SetAccessControl(security);
#endif

var ownsHandle = false;
try
{
try
{
ownsHandle = mutex.WaitOne(5000, false); // 5 sec
if (ownsHandle == false)
{
var timeoutMessage = string.Format(CultureInfo.InvariantCulture,
"Timeout waiting for exclusive access to '{0}'.", modulePathScintilla);
throw new TimeoutException(timeoutMessage);
}
}
catch (AbandonedMutexException)
{
// Previous process terminated abnormally
ownsHandle = true;
}

// Double-checked (process) lock
if (!File.Exists(modulePathScintilla))
{
// Write the embedded file to disk
var directory = Path.GetDirectoryName(modulePathScintilla);
if (directory != null && !Directory.Exists(directory))
Directory.CreateDirectory(directory);

#if SCINTILLA5
var resource = string.Format(CultureInfo.InvariantCulture,
$"{scintillaBaseName}.{(IntPtr.Size == 4 ? "x86" : "x64")}.Scintilla.zip");

using var resourceStream =
typeof(Scintilla).Assembly.GetManifestResourceStream(resource);

using var zipArchive = new ZipArchive(resourceStream, ZipArchiveMode.Read);

foreach (var entry in zipArchive.Entries)
{
if (entry.FullName == "Scintilla.dll")
{
entry.ExtractToFile(modulePathScintilla);
}

if (entry.FullName == "Lexilla.dll")
{
entry.ExtractToFile(modulePathLexilla);
}
}

#elif SCINTILLA4
if (!Directory.Exists(directory))
Directory.CreateDirectory(directory);

var resource =
string.Format(CultureInfo.InvariantCulture, $"{scintillaName}.{(IntPtr.Size == 4 ? "x86" : "x64")}.SciLexer.dll.gz");
using (var resourceStream =
typeof(Scintilla).Assembly.GetManifestResourceStream(resource))
using (var gzipStream = new GZipStream(resourceStream, CompressionMode.Decompress))
using (var fileStream = File.Create(modulePathScintilla))
gzipStream.CopyTo(fileStream);
#endif
}
}
finally
{
if (ownsHandle)
mutex.ReleaseMutex();
}
}
}

#if SCINTILLA5
try
{
var info = FileVersionInfo.GetVersionInfo(modulePathScintilla);
scintillaVersion = info.ProductVersion ?? info.FileVersion;
info = FileVersionInfo.GetVersionInfo(modulePathLexilla);
lexillaVersion = info.ProductVersion ?? info.FileVersion;
}
catch
{
scintillaVersion = "ERROR";
lexillaVersion = "ERROR";
}
#endif
}

return modulePathScintilla;

}

#if SCINTILLA5
private static string scintillaVersion;
private static string lexillaVersion;
private static readonly string scintillaVersion;
private static readonly string lexillaVersion;

/// <summary>
/// Gets the product version of the Scintilla.dll user by the control.
Expand Down Expand Up @@ -1361,8 +1246,7 @@ public string GetTextRangeAsHtml(int position, int length)
/// <returns>An object representing the version information of the native Scintilla library.</returns>
public FileVersionInfo GetVersionInfo()
{
var path = GetModulePath();
var version = FileVersionInfo.GetVersionInfo(path);
var version = FileVersionInfo.GetVersionInfo(modulePathScintilla);

return version;
}
Expand Down Expand Up @@ -2549,22 +2433,6 @@ public static void SetDestroyHandleBehavior(bool reparent)
}
}

/// <summary>
/// Sets the application-wide default module path of the native Scintilla library.
/// </summary>
/// <param name="modulePath">The native Scintilla module path.</param>
/// <remarks>
/// This method must be called prior to the first <see cref="Scintilla" /> control being created.
/// The <paramref name="modulePath" /> can be relative or absolute.
/// </remarks>
public static void SetModulePath(string modulePath)
{
if (Scintilla.modulePathScintilla == null)
{
Scintilla.modulePathScintilla = modulePath;
}
}

/// <summary>
/// Passes the specified property name-value pair to the current <see cref="Lexer" />.
/// </summary>
Expand Down Expand Up @@ -4145,18 +4013,16 @@ protected override CreateParams CreateParams
{
if (moduleHandle == IntPtr.Zero)
{
var path = GetModulePath();

// Load the native Scintilla library
moduleHandle = NativeMethods.LoadLibrary(path);
moduleHandle = NativeMethods.LoadLibrary(modulePathScintilla);

#if SCINTILLA5
lexillaHandle = NativeMethods.LoadLibrary(modulePathLexilla);
#endif

if (moduleHandle == IntPtr.Zero)
{
var message = string.Format(CultureInfo.InvariantCulture, "Could not load the Scintilla module at the path '{0}'.", path);
var message = string.Format(CultureInfo.InvariantCulture, "Could not load the Scintilla module at the path '{0}'.", modulePathScintilla);
throw new Win32Exception(message, new Win32Exception()); // Calls GetLastError
}

Expand Down
Loading

0 comments on commit a54befc

Please sign in to comment.