Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,46 @@ SQLEXTENSION_INTERFACE SQLRETURN CleanupSession(SQLGUID sessionId, SQLUSMALLINT
//
SQLEXTENSION_INTERFACE SQLRETURN Cleanup();

// Installs an external library to the specified directory.
//
// Dispatch is by the registered library name (libraryName), not the
// contents of libraryFile:
// - libraryName ending in ".zip" -> ZIP archive install. If the archive
// contains a single inner zip, that inner zip is extracted to the
// install directory; otherwise the outer archive's files are copied
// directly. A "{libName}.manifest" listing every extracted file is
// written so UninstallExternalLibrary can clean up exactly what was
// installed.
// - libraryName ending in ".dll" -> raw DLL install. libraryFile is
// copied verbatim to "{installDir}\{libraryName}" and a one-entry
// manifest is written.
// - libraryName with neither extension -> falls back to libraryFile's
// extension (preserves the legacy contract for callers that register
// libraries by bare name and point libraryFile at a "*.zip" or
// "*.dll" fixture).
//
SQLEXTENSION_INTERFACE SQLRETURN InstallExternalLibrary(
SQLGUID setupSessionId,
SQLCHAR *libraryName,
SQLINTEGER libraryNameLength,
SQLCHAR *libraryFile,
SQLINTEGER libraryFileLength,
SQLCHAR *libraryInstallDirectory,
SQLINTEGER libraryInstallDirectoryLength,
SQLCHAR **libraryError,
SQLINTEGER *libraryErrorLength);

// Uninstalls an external library from the specified directory.
//
SQLEXTENSION_INTERFACE SQLRETURN UninstallExternalLibrary(
SQLGUID setupSessionId,
SQLCHAR *libraryName,
SQLINTEGER libraryNameLength,
SQLCHAR *libraryInstallDirectory,
SQLINTEGER libraryInstallDirectoryLength,
SQLCHAR **libraryError,
SQLINTEGER *libraryErrorLength);

// Receives host callback function pointers from the host.
// Optional API, supported since v3 of the Extension API.
//
Expand Down
1,209 changes: 1,209 additions & 0 deletions language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -190,13 +190,31 @@ DataFrameColumn column
SetDataPtrs<byte>(columnNumber, GetStringArray(column));
break;
case SqlDataType.DotNetWChar:
case SqlDataType.DotNetNVarChar:
// Calculate column size from actual data.
// columnSize = max character count (UTF-16 byte length / 2).
// Minimum size is 1 character (nchar(0) is illegal in SQL).
// GetStrLenNullMap returns the per-row byte length
// (Encoding.Unicode.GetByteCount) and the SQL_C_WCHAR
// ODBC contract requires the column Size and the
// strLenOrNullMap entries to use the same unit. Report
// Size in BYTES; do NOT convert to a character count
// here -- a character-count Size combined with a
// byte-count length map causes SPEES to log
// "Reading one row failed for column N row M. The
// length information is incorrect." and reject the
// rowset.
//
// DotNetNVarChar is treated as an alias of DotNetWChar
// (both are SQL_C_WCHAR-shaped at the ODBC layer).
// Without this case, callers who set
// DataTypeMap[typeof(string)] = SqlDataType.DotNetNVarChar
// hit a KeyNotFoundException in DataTypeSize and the
// column never reaches the dispatch switch.
//
// Minimum size is 2 bytes (one UTF-16 code unit -- nchar(0)
// is illegal in SQL).
//
int maxUnicodeByteLen = colMap.Length > 0 ? colMap.Where(x => x > 0).DefaultIfEmpty(0).Max() : 0;
int maxCharCount = maxUnicodeByteLen / sizeof(char);
_columns[columnNumber].Size = (ulong)Math.Max(maxCharCount, MinUtf16CharSize);
_columns[columnNumber].Size = (ulong)Math.Max(maxUnicodeByteLen, MinUtf16CharSize);

SetDataPtrs<char>(columnNumber, GetUnicodeStringArray(column));
break;
Expand Down Expand Up @@ -429,8 +447,11 @@ private int[] GetStrLenNullMap(ushort columnNumber, DataFrameColumn column)
Logging.Trace($"GetStrLenNullMap: Row {rowNumber}, Value='{column[rowNumber]}', ByteLen={colMap[rowNumber]}");
break;
case SqlDataType.DotNetWChar:
case SqlDataType.DotNetNVarChar:
// Report the byte length of the UTF-16 encoded string (2 bytes per code unit).
// This must match the buffer size emitted by GetUnicodeStringArray().
// This must match the buffer size emitted by GetUnicodeStringArray()
// and the column Size set in ExtractColumn (also bytes for SQL_C_WCHAR).
// DotNetNVarChar is an alias of DotNetWChar at the ODBC layer.
//
colMap[rowNumber] = Encoding.Unicode.GetByteCount((string)column[rowNumber]);
Logging.Trace($"GetStrLenNullMap: Row {rowNumber}, Value='{column[rowNumber]}', ByteLen={colMap[rowNumber]}");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,15 +91,12 @@ public static List<string> CreateDllList(
}
else
{
if (!string.IsNullOrEmpty(privatePath))
{
dllList.AddRange(Directory.GetFiles(privatePath, userLibName));
}

if (!string.IsNullOrEmpty(publicPath))
{
dllList.AddRange(Directory.GetFiles(publicPath, userLibName));
}
// Callers may pass either a bare library name ("regex") or an explicit
// filename ("Foo.dll"). Try the exact name first so filenames with
// extensions resolve correctly; fall back to the "{name}.*" wildcard
// for bare names.
AddMatches(privatePath, userLibName, dllList);
AddMatches(publicPath, userLibName, dllList);
}

if (dllList.Count == 0)
Expand All @@ -110,6 +107,63 @@ public static List<string> CreateDllList(
return dllList;
}

/// <summary>
/// Adds DLL matches for <paramref name="userLibName"/> under
/// <paramref name="searchPath"/>. Tries the exact name first (so callers
/// that pass "Foo.dll" resolve correctly) and falls back to all .dll
/// files whose stem equals <paramref name="userLibName"/> (so callers
/// that pass "Foo" still match "Foo.dll").
/// </summary>
/// <param name="searchPath">
/// Directory to search. Returns immediately if null/empty or absent;
/// missing public/private library paths are not an error -- the other
/// path may still yield matches.
/// </param>
/// <param name="userLibName">
/// Library name to match. May be a bare stem ("regex") or include a
/// .dll suffix ("Foo.dll"). Wildcards / path separators are not
/// expected -- the caller is responsible for validation.
/// </param>
/// <param name="dllList">
/// Output list to append discovered .dll paths to. Caller-owned;
/// AddMatches never clears or replaces.
/// </param>
/// <remarks>
/// We deliberately avoid <see cref="Directory.GetFiles(string, string)"/>'s
/// search-pattern argument and its Win32 <c>FindFirstFile</c>
/// wildcard semantics, which over-match in two well-known ways on
/// Windows: <c>"*.dll"</c> matches files like <c>"foo.dllx"</c>
/// (3-char-extension prefix-match quirk inherited from FAT), and
/// <c>"Foo.*"</c> can spuriously match short-name (8.3) aliases of
/// long-named files. Enumerating all entries and filtering with
/// <see cref="StringComparison.OrdinalIgnoreCase"/> equality on
/// both the extension and the stem gives exact, predictable
/// matches and avoids loading anything we did not intend.
/// </remarks>
private static void AddMatches(string searchPath, string userLibName, List<string> dllList)
Comment thread
yaelh marked this conversation as resolved.
{
if (string.IsNullOrEmpty(searchPath) || !Directory.Exists(searchPath))
{
return;
}

string exactPath = Path.Combine(searchPath, userLibName);
if (File.Exists(exactPath))
{
dllList.Add(exactPath);
return;
}

foreach (string f in Directory.EnumerateFiles(searchPath))
{
if (Path.GetExtension(f).Equals(".dll", StringComparison.OrdinalIgnoreCase) &&
Path.GetFileNameWithoutExtension(f).Equals(userLibName, StringComparison.OrdinalIgnoreCase))
{
dllList.Add(f);
}
}
}

/// <summary>
/// This method finds the corresponding loaded dll for user dll's dependencies.
/// It searches for the corresponding loaded dll that matches args.Name.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ public static short WrapError(Action func)
}
catch (Exception e)
{
Logging.Error(e.StackTrace + "Error: " + e.Message);
// Log full exception chain so inner exceptions are visible in satellite logs.
// TargetInvocationException (from Activator.CreateInstance reflection) wraps
// the real error — e.ToString() unwinds the whole chain.
Logging.Error(e.ToString());
return SQL_ERROR;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ public enum SqlDataType: short
{SqlDataType.DotNetBit, sizeof(bool)},
{SqlDataType.DotNetChar, MinUtf8CharSize},
{SqlDataType.DotNetWChar, MinUtf16CharSize},
{SqlDataType.DotNetNVarChar, MinUtf16CharSize},
{SqlDataType.DotNetNumeric, SqlNumericStructSize}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,159 @@ SQLRETURN Cleanup()
return SQL_SUCCESS;
}

//--------------------------------------------------------------------------------------------------
// Name: SetLibraryError
//
// Description:
// Helper to populate the library error output parameters.
//
static void SetLibraryError(
const std::string &errorString,
SQLCHAR **libraryError,
SQLINTEGER *libraryErrorLength)
{
// Guard against null out-parameters. The managed SetLibraryError
// (CSharpExtension.cs) does the same null-check; without it, a caller
// passing null libraryError / libraryErrorLength would dereference null
// and crash the host process.
if (libraryError == nullptr || libraryErrorLength == nullptr)
{
return;
}

if (!errorString.empty())
Comment thread
stuartpa marked this conversation as resolved.
Comment thread
stuartpa marked this conversation as resolved.
{
// Length excludes null terminator -- ExtHost adds +1 when copying
// (see Utf8ToNullTerminatedUtf16Le / strcpy_s in the host).
*libraryErrorLength = static_cast<SQLINTEGER>(errorString.length());

// Ownership of the buffer transfers to ExtHost; it frees the pointer
// via the C runtime (free()) after consuming the message. Mirrors
// the managed SetLibraryError (CSharpExtension.cs), which uses
// Marshal.AllocHGlobal for the same reason. The previous
// implementation -- new std::string(errorString) + returning
// c_str() -- leaked the string AND handed back a pointer into a
// std::string's internal buffer, which is undefined behavior the
// moment ExtHost calls free() on it.
size_t bufLen = errorString.length() + 1;
char *buf = static_cast<char*>(malloc(bufLen));
if (buf == nullptr)
{
// Out of memory; surface a "no error" state rather than crash
// (the original failure will still be logged by the caller).
*libraryError = nullptr;
*libraryErrorLength = 0;
return;
}
memcpy(buf, errorString.c_str(), bufLen);
*libraryError = reinterpret_cast<SQLCHAR*>(buf);
}
else
{
// Explicitly clear the out-parameters so callers that don't
// pre-initialize them see a well-defined "no error" state.
*libraryError = nullptr;
*libraryErrorLength = 0;
}
}

//--------------------------------------------------------------------------------------------------
// Name: InstallExternalLibrary
//
// Description:
// Installs an external library to the specified directory.
// The library file may be a ZIP archive or a raw DLL.
// If a ZIP, and it contains an inner zip, that inner zip is extracted to the
// install directory. Otherwise, all files are copied directly.
//
// Returns:
// SQL_SUCCESS on success, else SQL_ERROR
//
SQLRETURN InstallExternalLibrary(
SQLGUID setupSessionId,
SQLCHAR *libraryName,
SQLINTEGER libraryNameLength,
SQLCHAR *libraryFile,
SQLINTEGER libraryFileLength,
SQLCHAR *libraryInstallDirectory,
SQLINTEGER libraryInstallDirectoryLength,
SQLCHAR **libraryError,
SQLINTEGER *libraryErrorLength)
{
LOG("nativecsharpextension::InstallExternalLibrary");

SQLRETURN result = SQL_ERROR;

if (g_dotnet_runtime == nullptr)
{
SetLibraryError(
"Extension not initialized. Call Init before InstallExternalLibrary.",
libraryError,
libraryErrorLength);
}
else
{
result = g_dotnet_runtime->call_managed_method<decltype(&InstallExternalLibrary)>(
nameof(InstallExternalLibrary),
setupSessionId,
libraryName,
libraryNameLength,
libraryFile,
libraryFileLength,
libraryInstallDirectory,
libraryInstallDirectoryLength,
libraryError,
libraryErrorLength);
}

return result;
}

//--------------------------------------------------------------------------------------------------
// Name: UninstallExternalLibrary
//
// Description:
// Uninstalls an external library from the specified directory.
//
// Returns:
// SQL_SUCCESS on success, else SQL_ERROR
//
SQLRETURN UninstallExternalLibrary(
SQLGUID setupSessionId,
SQLCHAR *libraryName,
SQLINTEGER libraryNameLength,
SQLCHAR *libraryInstallDirectory,
SQLINTEGER libraryInstallDirectoryLength,
SQLCHAR **libraryError,
SQLINTEGER *libraryErrorLength)
{
LOG("nativecsharpextension::UninstallExternalLibrary");

SQLRETURN result = SQL_ERROR;

if (g_dotnet_runtime == nullptr)
{
SetLibraryError(
"Extension not initialized. Call Init before UninstallExternalLibrary.",
libraryError,
libraryErrorLength);
}
else
{
result = g_dotnet_runtime->call_managed_method<decltype(&UninstallExternalLibrary)>(
nameof(UninstallExternalLibrary),
setupSessionId,
libraryName,
libraryNameLength,
libraryInstallDirectory,
libraryInstallDirectoryLength,
libraryError,
libraryErrorLength);
}

return result;
}

//--------------------------------------------------------------------------------------------------
// Name: SetHostCallbacks
//
Expand Down
Loading