From 15aa08e49ceba4bc572682447ba89d0b9eadfe9a Mon Sep 17 00:00:00 2001 From: Miljan Veljovic Date: Wed, 27 May 2026 18:51:14 +0200 Subject: [PATCH 01/22] feat: add v3 of extension api with host callbacks --- extension-host/include/sqlexternallanguage.h | 36 +++++++++- .../include/nativecsharpextension.h | 12 +++- .../src/managed/CSharpExtension.cs | 70 +++++++++++++++++++ .../src/managed/utils/Logging.cs | 54 ++++++++++++++ .../src/native/nativecsharpextension.cpp | 31 ++++++++ 5 files changed, 201 insertions(+), 2 deletions(-) diff --git a/extension-host/include/sqlexternallanguage.h b/extension-host/include/sqlexternallanguage.h index 0709e39b..d6286e8c 100644 --- a/extension-host/include/sqlexternallanguage.h +++ b/extension-host/include/sqlexternallanguage.h @@ -28,7 +28,7 @@ extern "C" { #endif /* __cplusplus */ -#define EXTERNAL_LANGUAGE_EXTENSION_API 2 +#define EXTERNAL_LANGUAGE_EXTENSION_API 3 SQLEXTENSION_INTERFACE SQLUSMALLINT GetInterfaceVersion(); @@ -137,6 +137,40 @@ SQLRETURN CleanupSession( SQLEXTENSION_INTERFACE SQLRETURN Cleanup(); +// Callback function type for logging XEvents from an extension. +// The extension calls this function pointer (implemented by ExtHost) +// to fire XEvents from within the extension's code. +// +typedef void (*PFunc_ExtensionLogXEvent)( + SQLGUID SessionId, + SQLUSMALLINT TaskId, + SQLSMALLINT TraceLevel, + SQLINTEGER ErrorCode, + SQLCHAR *ExtensionName, + SQLULEN ExtensionNameLength, + SQLCHAR *Message, + SQLULEN MessageLength + ); + +// Host callbacks structure passed from ExtHost to the extension. +// The Version field allows future expansion without a new API version. +// +#define SQLEXTENSION_HOST_CALLBACKS_VERSION_1 1 + +typedef struct _SQLEXTENSION_HOST_CALLBACKS +{ + SQLUSMALLINT Version; + PFunc_ExtensionLogXEvent LogXEvent; +} SQLEXTENSION_HOST_CALLBACKS; + +// Optional API (v3+) +// Receives host callback functions from ExtHost. +// +SQLEXTENSION_INTERFACE +SQLRETURN SetHostCallbacks( + SQLEXTENSION_HOST_CALLBACKS *Callbacks + ); + #ifdef __cplusplus } /* End of extern "C" { */ #endif/* __cplusplus */ diff --git a/language-extensions/dotnet-core-CSharp/include/nativecsharpextension.h b/language-extensions/dotnet-core-CSharp/include/nativecsharpextension.h index 8a1a3412..6afc7ee3 100644 --- a/language-extensions/dotnet-core-CSharp/include/nativecsharpextension.h +++ b/language-extensions/dotnet-core-CSharp/include/nativecsharpextension.h @@ -142,6 +142,16 @@ SQLEXTENSION_INTERFACE SQLRETURN CleanupSession(SQLGUID sessionId, SQLUSMALLINT // SQLEXTENSION_INTERFACE SQLRETURN Cleanup(); +// Receives host callback function pointers from ExtHost. +// Optional API, supported since v3 of the extension API. +// +SQLEXTENSION_INTERFACE SQLRETURN SetHostCallbacks( + SQLEXTENSION_HOST_CALLBACKS *hostCallbacks); + // Dotnet environment pointer // -static DotnetEnvironment* g_dotnet_runtime = nullptr; \ No newline at end of file +static DotnetEnvironment* g_dotnet_runtime = nullptr; + +// Host callbacks pointer provided by ExtHost via SetHostCallbacks. +// +static SQLEXTENSION_HOST_CALLBACKS* g_hostCallbacks = nullptr; \ No newline at end of file diff --git a/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs b/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs index 0f3be1f3..58a38b43 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs @@ -601,6 +601,76 @@ public static short GetOutputParam( }); } + /// + /// Delegate type matching the host's LogXEvent callback signature. + /// + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void LogXEventCallbackDelegate( + Guid sessionId, + ushort taskId, + short traceLevel, + int errorCode, + char *extensionName, + ulong extensionNameLength, + char *message, + ulong messageLength); + + /// + /// Managed representation of the SQLEXTENSION_HOST_CALLBACKS structure. + /// Must match the native struct layout exactly. + /// + [StructLayout(LayoutKind.Sequential)] + public struct SqlExtensionHostCallbacks + { + public ushort Version; + public IntPtr LogXEvent; + } + + /// + /// This delegate declares the delegate type of SetHostCallbacks. + /// + public delegate short SetHostCallbacksDelegate( + SqlExtensionHostCallbacks *hostCallbacks); + + /// + /// This method implements SetHostCallbacks API. + /// Receives a pointer to the host callbacks structure and stores the + /// callback function pointers so managed code can call back into the host. + /// + /// + /// Pointer to the SQLEXTENSION_HOST_CALLBACKS structure provided by the host. + /// The host owns this memory and keeps it alive for the extension's lifetime. + /// + /// + /// SQL_SUCCESS(0), SQL_ERROR(-1) + /// + public static short SetHostCallbacks( + SqlExtensionHostCallbacks *hostCallbacks) + { + Logging.Trace("CSharpExtension::SetHostCallbacks"); + return ExceptionUtils.WrapError(() => + { + if (hostCallbacks == null) + { + throw new ArgumentNullException(nameof(hostCallbacks)); + } + + if (hostCallbacks->LogXEvent != IntPtr.Zero) + { + var logXEvent = Marshal.GetDelegateForFunctionPointer( + hostCallbacks->LogXEvent); + Logging.SetLogXEventCallback(logXEvent); + + Logging.LogXEvent( + Guid.Empty, + taskId: 0, + traceLevel: 0, + errorCode: 0, + "CSharp extension loaded, host callbacks registered (version " + hostCallbacks->Version + ")"); + } + }); + } + /// /// This delegate declares the delegate type of CleanupSession. /// diff --git a/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs b/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs index bdc36b9f..f807643c 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs @@ -30,6 +30,11 @@ class Logging /// private const int StdErr = 2; + /// + /// Host-provided LogXEvent callback, set via SetHostCallbacks. + /// + private static CSharpExtension.LogXEventCallbackDelegate _logXEventCallback; + /// /// Static constructor to initialize the custom text writers for stdout and stderr. /// This ensures that any Console.WriteLine or Console.Error.WriteLine calls @@ -74,6 +79,55 @@ public static void Error(string message) } } + /// + /// Stores the host-provided LogXEvent callback for later use. + /// Called from CSharpExtension.SetHostCallbacks. + /// + /// The delegate wrapping the host's LogXEvent function pointer. + public static void SetLogXEventCallback(CSharpExtension.LogXEventCallbackDelegate callback) + { + _logXEventCallback = callback; + } + + /// + /// Returns true if the host has provided a LogXEvent callback. + /// + public static bool HasLogXEventCallback => _logXEventCallback != null; + + /// + /// Logs a message through the host's XEvent infrastructure. + /// If no host callback is registered, this is a no-op. + /// + /// Session GUID. + /// Task identifier. + /// Trace level (severity). + /// Error code (0 for informational). + /// The message to log. + public static unsafe void LogXEvent( + Guid sessionId, + ushort taskId, + short traceLevel, + int errorCode, + string message) + { + if (_logXEventCallback == null) + return; + + byte[] utf8Bytes = Encoding.UTF8.GetBytes(message ?? string.Empty); + fixed (byte* pBytes = utf8Bytes) + { + _logXEventCallback( + sessionId, + taskId, + traceLevel, + errorCode, + null, + 0, + (char*)pBytes, + (ulong)utf8Bytes.Length); + } + } + /// /// Custom TextWriter that writes directly to the standard output/error streams /// using native system calls. This is required because the standard Console diff --git a/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp b/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp index b70831d7..9e2f7fc9 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp @@ -338,6 +338,37 @@ SQLRETURN CleanupSession(SQLGUID sessionId, SQLUSMALLINT taskId) SQLRETURN Cleanup() { LOG("nativecsharpextension::Cleanup"); + g_hostCallbacks = nullptr; delete g_dotnet_runtime; return SQL_SUCCESS; +} + +//-------------------------------------------------------------------------------------------------- +// Name: SetHostCallbacks +// +// Description: +// Receives a pointer to the host callbacks structure from ExtHost. +// Stores the pointer natively and forwards to managed code so the +// managed layer can call back into the host (e.g. for XEvent logging). +// +// Returns: +// SQL_SUCCESS on success, else SQL_ERROR +// +SQLRETURN SetHostCallbacks( + SQLEXTENSION_HOST_CALLBACKS *hostCallbacks +) +{ + LOG("nativecsharpextension::SetHostCallbacks"); + + if (hostCallbacks == nullptr) + { + LOG_ERROR("SetHostCallbacks called with null pointer"); + return SQL_ERROR; + } + + g_hostCallbacks = hostCallbacks; + + return g_dotnet_runtime->call_managed_method( + nameof(SetHostCallbacks), + hostCallbacks); } \ No newline at end of file From ffc94f4bdf6a61eb056c0844f1f7478558adec0c Mon Sep 17 00:00:00 2001 From: Miljan Veljovic Date: Wed, 27 May 2026 20:25:53 +0200 Subject: [PATCH 02/22] feat: remove extension name from parameters list --- extension-host/include/sqlexternallanguage.h | 2 -- .../dotnet-core-CSharp/src/managed/CSharpExtension.cs | 2 -- .../dotnet-core-CSharp/src/managed/utils/Logging.cs | 2 -- 3 files changed, 6 deletions(-) diff --git a/extension-host/include/sqlexternallanguage.h b/extension-host/include/sqlexternallanguage.h index d6286e8c..e26059f7 100644 --- a/extension-host/include/sqlexternallanguage.h +++ b/extension-host/include/sqlexternallanguage.h @@ -146,8 +146,6 @@ typedef void (*PFunc_ExtensionLogXEvent)( SQLUSMALLINT TaskId, SQLSMALLINT TraceLevel, SQLINTEGER ErrorCode, - SQLCHAR *ExtensionName, - SQLULEN ExtensionNameLength, SQLCHAR *Message, SQLULEN MessageLength ); diff --git a/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs b/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs index 58a38b43..154b3029 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs @@ -610,8 +610,6 @@ public delegate void LogXEventCallbackDelegate( ushort taskId, short traceLevel, int errorCode, - char *extensionName, - ulong extensionNameLength, char *message, ulong messageLength); diff --git a/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs b/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs index f807643c..bb235fe4 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs @@ -121,8 +121,6 @@ public static unsafe void LogXEvent( taskId, traceLevel, errorCode, - null, - 0, (char*)pBytes, (ulong)utf8Bytes.Length); } From effc82920265db42ddefa8da8c3f0e57da436ecb Mon Sep 17 00:00:00 2001 From: Miljan Veljovic Date: Thu, 28 May 2026 00:58:10 +0200 Subject: [PATCH 03/22] Revert "feat: remove extension name from parameters list" This reverts commit 7b6bdb396c9ff09c4ff2f5b7f153a391bff24281. --- extension-host/include/sqlexternallanguage.h | 2 ++ .../dotnet-core-CSharp/src/managed/CSharpExtension.cs | 2 ++ .../dotnet-core-CSharp/src/managed/utils/Logging.cs | 2 ++ 3 files changed, 6 insertions(+) diff --git a/extension-host/include/sqlexternallanguage.h b/extension-host/include/sqlexternallanguage.h index e26059f7..d6286e8c 100644 --- a/extension-host/include/sqlexternallanguage.h +++ b/extension-host/include/sqlexternallanguage.h @@ -146,6 +146,8 @@ typedef void (*PFunc_ExtensionLogXEvent)( SQLUSMALLINT TaskId, SQLSMALLINT TraceLevel, SQLINTEGER ErrorCode, + SQLCHAR *ExtensionName, + SQLULEN ExtensionNameLength, SQLCHAR *Message, SQLULEN MessageLength ); diff --git a/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs b/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs index 154b3029..58a38b43 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs @@ -610,6 +610,8 @@ public delegate void LogXEventCallbackDelegate( ushort taskId, short traceLevel, int errorCode, + char *extensionName, + ulong extensionNameLength, char *message, ulong messageLength); diff --git a/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs b/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs index bb235fe4..f807643c 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs @@ -121,6 +121,8 @@ public static unsafe void LogXEvent( taskId, traceLevel, errorCode, + null, + 0, (char*)pBytes, (ulong)utf8Bytes.Length); } From 4aa44e807042fd3dabc15be53b663870378a699b Mon Sep 17 00:00:00 2001 From: Miljan Veljovic Date: Thu, 28 May 2026 16:14:25 +0200 Subject: [PATCH 04/22] fix: adjust parameters --- extension-host/include/sqlexternallanguage.h | 21 +++++++++--- .../src/managed/CSharpExtension.cs | 8 ++--- .../src/managed/utils/Logging.cs | 33 ++++++++++++++----- 3 files changed, 46 insertions(+), 16 deletions(-) diff --git a/extension-host/include/sqlexternallanguage.h b/extension-host/include/sqlexternallanguage.h index d6286e8c..399256e3 100644 --- a/extension-host/include/sqlexternallanguage.h +++ b/extension-host/include/sqlexternallanguage.h @@ -137,18 +137,31 @@ SQLRETURN CleanupSession( SQLEXTENSION_INTERFACE SQLRETURN Cleanup(); +// Trace levels for events logged from an extension via PFunc_ExtensionLogXEvent. +// Lowest numeric value is the most severe, matching the Windows ETW +// TRACE_LEVEL_* convention. Must match ExtHost's ExtensionTraceLevel enum. +// +enum ExtensionTraceLevel +{ + Extension_Critical = 1, + Extension_Error = 2, + Extension_Warning = 3, + Extension_Information = 4, + Extension_Verbose = 5 +}; + // Callback function type for logging XEvents from an extension. // The extension calls this function pointer (implemented by ExtHost) // to fire XEvents from within the extension's code. // typedef void (*PFunc_ExtensionLogXEvent)( + const SQLCHAR *ExtensionName, + SQLULEN ExtensionNameLength, SQLGUID SessionId, SQLUSMALLINT TaskId, - SQLSMALLINT TraceLevel, + SQLUSMALLINT TraceLevel, SQLINTEGER ErrorCode, - SQLCHAR *ExtensionName, - SQLULEN ExtensionNameLength, - SQLCHAR *Message, + const SQLCHAR *Message, SQLULEN MessageLength ); diff --git a/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs b/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs index 58a38b43..174b62fe 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs @@ -606,12 +606,12 @@ public static short GetOutputParam( /// [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void LogXEventCallbackDelegate( + char *extensionName, + ulong extensionNameLength, Guid sessionId, ushort taskId, - short traceLevel, + ushort traceLevel, int errorCode, - char *extensionName, - ulong extensionNameLength, char *message, ulong messageLength); @@ -664,7 +664,7 @@ public static short SetHostCallbacks( Logging.LogXEvent( Guid.Empty, taskId: 0, - traceLevel: 0, + traceLevel: Logging.TraceLevel.Information, errorCode: 0, "CSharp extension loaded, host callbacks registered (version " + hostCallbacks->Version + ")"); } diff --git a/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs b/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs index f807643c..fa098dab 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs @@ -94,6 +94,21 @@ public static void SetLogXEventCallback(CSharpExtension.LogXEventCallbackDelegat /// public static bool HasLogXEventCallback => _logXEventCallback != null; + /// + /// Trace levels for events logged via the host's LogXEvent callback. + /// Lowest numeric value is the most severe, matching Windows ETW TRACE_LEVEL_* convention. + /// + public enum TraceLevel : ushort + { + Critical = 1, + Error = 2, + Warning = 3, + Information = 4, + Verbose = 5, + } + + private const string ExtensionName = "CSharp"; + /// /// Logs a message through the host's XEvent infrastructure. /// If no host callback is registered, this is a no-op. @@ -104,25 +119,27 @@ public static void SetLogXEventCallback(CSharpExtension.LogXEventCallbackDelegat /// Error code (0 for informational). /// The message to log. public static unsafe void LogXEvent( - Guid sessionId, - ushort taskId, - short traceLevel, - int errorCode, - string message) + Guid sessionId, + ushort taskId, + TraceLevel traceLevel, + int errorCode, + string message) { if (_logXEventCallback == null) return; + byte[] utf8ExtName = Encoding.UTF8.GetBytes(ExtensionName); byte[] utf8Bytes = Encoding.UTF8.GetBytes(message ?? string.Empty); + fixed (byte* pExtName = utf8ExtName) fixed (byte* pBytes = utf8Bytes) { _logXEventCallback( + (char*)pExtName, + (ulong)utf8ExtName.Length, sessionId, taskId, - traceLevel, + (ushort)traceLevel, errorCode, - null, - 0, (char*)pBytes, (ulong)utf8Bytes.Length); } From d62a9debf5d8e8e9de7081783a53e61fb5727612 Mon Sep 17 00:00:00 2001 From: Miljan Veljovic Date: Thu, 28 May 2026 16:20:37 +0200 Subject: [PATCH 05/22] chore: comments --- extension-host/include/sqlexternallanguage.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extension-host/include/sqlexternallanguage.h b/extension-host/include/sqlexternallanguage.h index 399256e3..932ea2c4 100644 --- a/extension-host/include/sqlexternallanguage.h +++ b/extension-host/include/sqlexternallanguage.h @@ -137,9 +137,9 @@ SQLRETURN CleanupSession( SQLEXTENSION_INTERFACE SQLRETURN Cleanup(); -// Trace levels for events logged from an extension via PFunc_ExtensionLogXEvent. +// Trace levels for events logged from via LogXEvent function. // Lowest numeric value is the most severe, matching the Windows ETW -// TRACE_LEVEL_* convention. Must match ExtHost's ExtensionTraceLevel enum. +// TRACE_LEVEL_* convention. // enum ExtensionTraceLevel { From 0adb8695116a1500645b3f12642ee65c10837b20 Mon Sep 17 00:00:00 2001 From: Miljan Veljovic Date: Thu, 28 May 2026 16:54:30 +0200 Subject: [PATCH 06/22] feat: add test --- .../native/CSharpSetHostCallbacksTests.cpp | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 language-extensions/dotnet-core-CSharp/test/src/native/CSharpSetHostCallbacksTests.cpp diff --git a/language-extensions/dotnet-core-CSharp/test/src/native/CSharpSetHostCallbacksTests.cpp b/language-extensions/dotnet-core-CSharp/test/src/native/CSharpSetHostCallbacksTests.cpp new file mode 100644 index 00000000..289e3d3f --- /dev/null +++ b/language-extensions/dotnet-core-CSharp/test/src/native/CSharpSetHostCallbacksTests.cpp @@ -0,0 +1,167 @@ +//********************************************************************* +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// +// @File: CSharpSetHostCallbacksTests.cpp +// +// Purpose: +// Tests the optional SetHostCallbacks API exported by the .NET Core C# +// extension. Verifies symbol resolution, null-pointer handling, and that +// a non-null LogXEvent callback supplied via SQLEXTENSION_HOST_CALLBACKS +// is invoked by managed code (callback registration + forwarding). +// +//********************************************************************* +#include "CSharpExtensionApiTests.h" + +#include +#include +#include + +using namespace std; + +namespace ExtensionApiTest +{ + typedef SQLRETURN FN_setHostCallbacks(SQLEXTENSION_HOST_CALLBACKS *); + + namespace + { + // Captured invocation of the host LogXEvent callback. + // + struct CapturedLogEvent + { + string extensionName; + SQLUSMALLINT traceLevel; + SQLINTEGER errorCode; + string message; + }; + + // File-scope storage for events captured by TestLogXEventCallback. + // Cleared at the start of each test that uses it. + // + static vector g_capturedLogEvents; + + // Test stand-in for ExtHost's LogXEvent implementation. Records the + // invocation so the test can assert on its contents. + // + extern "C" void TestLogXEventCallback( + const SQLCHAR *extensionName, + SQLULEN extensionNameLength, + SQLGUID /*sessionId*/, + SQLUSMALLINT /*taskId*/, + SQLUSMALLINT traceLevel, + SQLINTEGER errorCode, + const SQLCHAR *message, + SQLULEN messageLength) + { + CapturedLogEvent ev; + if (extensionName != nullptr && extensionNameLength > 0) + { + ev.extensionName.assign( + reinterpret_cast(extensionName), + static_cast(extensionNameLength)); + } + ev.traceLevel = traceLevel; + ev.errorCode = errorCode; + if (message != nullptr && messageLength > 0) + { + ev.message.assign( + reinterpret_cast(message), + static_cast(messageLength)); + } + g_capturedLogEvents.push_back(std::move(ev)); + } + } + +#define RESOLVE_SET_HOST_CALLBACKS() \ + reinterpret_cast( \ + GetProcAddress(sm_libHandle, "SetHostCallbacks")) + + //---------------------------------------------------------------------------------------------- + // Name: SetHostCallbacks_SymbolIsExported + // + // Description: + // Verifies the optional SetHostCallbacks entry point is exported from + // nativecsharpextension so ExtHost can discover it via GetProcAddress. + // + TEST_F(CSharpExtensionApiTests, SetHostCallbacks_SymbolIsExported) + { + FN_setHostCallbacks *fn = RESOLVE_SET_HOST_CALLBACKS(); + EXPECT_NE(fn, nullptr) + << "SetHostCallbacks must be exported by the extension binary"; + } + + //---------------------------------------------------------------------------------------------- + // Name: SetHostCallbacks_NullPointerReturnsError + // + // Description: + // Passing a null SQLEXTENSION_HOST_CALLBACKS* must be rejected with + // SQL_ERROR rather than crashing the host. + // + TEST_F(CSharpExtensionApiTests, SetHostCallbacks_NullPointerReturnsError) + { + FN_setHostCallbacks *fn = RESOLVE_SET_HOST_CALLBACKS(); + ASSERT_NE(fn, nullptr); + + SQLRETURN rc = fn(nullptr); + EXPECT_EQ(rc, SQL_ERROR); + } + + //---------------------------------------------------------------------------------------------- + // Name: SetHostCallbacks_RegistersAndForwardsLogXEvent + // + // Description: + // Provides a non-null LogXEvent callback and verifies that managed + // SetHostCallbacks registers it and forwards an invocation back through + // it (the managed implementation emits an "extension loaded" XEvent + // immediately after registration), proving end-to-end callback wiring. + // + TEST_F(CSharpExtensionApiTests, SetHostCallbacks_RegistersAndForwardsLogXEvent) + { + FN_setHostCallbacks *fn = RESOLVE_SET_HOST_CALLBACKS(); + ASSERT_NE(fn, nullptr); + + g_capturedLogEvents.clear(); + + SQLEXTENSION_HOST_CALLBACKS hostCallbacks{}; + hostCallbacks.Version = SQLEXTENSION_HOST_CALLBACKS_VERSION_1; + hostCallbacks.LogXEvent = &TestLogXEventCallback; + + SQLRETURN rc = fn(&hostCallbacks); + EXPECT_EQ(rc, SQL_SUCCESS); + + ASSERT_FALSE(g_capturedLogEvents.empty()) + << "Managed SetHostCallbacks should invoke the host LogXEvent " + "callback at least once after registration"; + + const CapturedLogEvent &ev = g_capturedLogEvents.front(); + EXPECT_EQ(ev.extensionName, string("CSharp")); + EXPECT_EQ(ev.traceLevel, static_cast(Extension_Information)); + EXPECT_EQ(ev.errorCode, 0); + EXPECT_NE(ev.message.find("CSharp extension loaded"), string::npos) + << "Unexpected message: " << ev.message; + } + + //---------------------------------------------------------------------------------------------- + // Name: SetHostCallbacks_NullLogXEventIsAccepted + // + // Description: + // A callbacks struct with a null LogXEvent slot is valid (host opted out + // of logging). SetHostCallbacks must succeed and must not invoke any + // callback. + // + TEST_F(CSharpExtensionApiTests, SetHostCallbacks_NullLogXEventIsAccepted) + { + FN_setHostCallbacks *fn = RESOLVE_SET_HOST_CALLBACKS(); + ASSERT_NE(fn, nullptr); + + g_capturedLogEvents.clear(); + + SQLEXTENSION_HOST_CALLBACKS hostCallbacks{}; + hostCallbacks.Version = SQLEXTENSION_HOST_CALLBACKS_VERSION_1; + hostCallbacks.LogXEvent = nullptr; + + SQLRETURN rc = fn(&hostCallbacks); + EXPECT_EQ(rc, SQL_SUCCESS); + EXPECT_TRUE(g_capturedLogEvents.empty()); + } +} From 058e1a0075f535aabc952d2aa822d3ed35831736 Mon Sep 17 00:00:00 2001 From: Miljan Veljovic Date: Thu, 28 May 2026 16:59:29 +0200 Subject: [PATCH 07/22] chore: comments --- .../test/src/native/CSharpSetHostCallbacksTests.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/test/src/native/CSharpSetHostCallbacksTests.cpp b/language-extensions/dotnet-core-CSharp/test/src/native/CSharpSetHostCallbacksTests.cpp index 289e3d3f..ff4a85ed 100644 --- a/language-extensions/dotnet-core-CSharp/test/src/native/CSharpSetHostCallbacksTests.cpp +++ b/language-extensions/dotnet-core-CSharp/test/src/native/CSharpSetHostCallbacksTests.cpp @@ -40,14 +40,14 @@ namespace ExtensionApiTest // static vector g_capturedLogEvents; - // Test stand-in for ExtHost's LogXEvent implementation. Records the + // Test stand-in for host's LogXEvent implementation. Records the // invocation so the test can assert on its contents. // extern "C" void TestLogXEventCallback( const SQLCHAR *extensionName, SQLULEN extensionNameLength, - SQLGUID /*sessionId*/, - SQLUSMALLINT /*taskId*/, + SQLGUID sessionId, + SQLUSMALLINT taskId, SQLUSMALLINT traceLevel, SQLINTEGER errorCode, const SQLCHAR *message, @@ -81,7 +81,7 @@ namespace ExtensionApiTest // // Description: // Verifies the optional SetHostCallbacks entry point is exported from - // nativecsharpextension so ExtHost can discover it via GetProcAddress. + // nativecsharpextension so host can discover it via GetProcAddress. // TEST_F(CSharpExtensionApiTests, SetHostCallbacks_SymbolIsExported) { From 6c2ba568c1bccf151dc72733a239398e786a8499 Mon Sep 17 00:00:00 2001 From: Miljan Veljovic Date: Thu, 28 May 2026 17:08:35 +0200 Subject: [PATCH 08/22] fix: add version check --- .../src/managed/CSharpExtension.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs b/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs index 174b62fe..7506f4c8 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs @@ -626,6 +626,12 @@ public struct SqlExtensionHostCallbacks public IntPtr LogXEvent; } + /// + /// Highest SQLEXTENSION_HOST_CALLBACKS version this extension understands. + /// Must match SQLEXTENSION_HOST_CALLBACKS_VERSION_* in sqlexternallanguage.h. + /// + private const ushort MaxSupportedHostCallbacksVersion = 1; + /// /// This delegate declares the delegate type of SetHostCallbacks. /// @@ -655,6 +661,20 @@ public static short SetHostCallbacks( throw new ArgumentNullException(nameof(hostCallbacks)); } + // Validate the struct version before reading any version-gated fields. + // + if (hostCallbacks->Version == 0 || + hostCallbacks->Version > MaxSupportedHostCallbacksVersion) + { + Logging.Error( + "CSharpExtension::SetHostCallbacks: unsupported host callbacks version: " + + hostCallbacks->Version); + + throw new NotSupportedException( + "Unsupported SQLEXTENSION_HOST_CALLBACKS version: " + + hostCallbacks->Version); + } + if (hostCallbacks->LogXEvent != IntPtr.Zero) { var logXEvent = Marshal.GetDelegateForFunctionPointer( From 706081544a19596b9e9956b58a1a57eb5641655a Mon Sep 17 00:00:00 2001 From: Miljan Veljovic Date: Thu, 28 May 2026 17:10:40 +0200 Subject: [PATCH 09/22] fix: set LogXEvent callback to nullptr if not provided by the host --- .../dotnet-core-CSharp/src/managed/CSharpExtension.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs b/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs index 7506f4c8..914d76c8 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs @@ -688,6 +688,13 @@ public static short SetHostCallbacks( errorCode: 0, "CSharp extension loaded, host callbacks registered (version " + hostCallbacks->Version + ")"); } + else + { + // Host opted out of XEvent logging. + // Clear any previously stored delegate. + // + Logging.SetLogXEventCallback(null); + } }); } From 7329d0308618eb23266bcf8d84adc1504c7e8831 Mon Sep 17 00:00:00 2001 From: Miljan Veljovic Date: Thu, 28 May 2026 17:15:08 +0200 Subject: [PATCH 10/22] fix: address nullptr concern --- .../src/native/nativecsharpextension.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp b/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp index 9e2f7fc9..e0a00aa4 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp @@ -340,6 +340,7 @@ SQLRETURN Cleanup() LOG("nativecsharpextension::Cleanup"); g_hostCallbacks = nullptr; delete g_dotnet_runtime; + g_dotnet_runtime = nullptr; return SQL_SUCCESS; } @@ -366,6 +367,12 @@ SQLRETURN SetHostCallbacks( return SQL_ERROR; } + if (g_dotnet_runtime == nullptr) + { + LOG_ERROR("SetHostCallbacks called before Init() or after Cleanup()"); + return SQL_ERROR; + } + g_hostCallbacks = hostCallbacks; return g_dotnet_runtime->call_managed_method( From 8d50d54b90eb07c4daca705194139533cf7633bf Mon Sep 17 00:00:00 2001 From: Miljan Veljovic Date: Thu, 28 May 2026 17:24:32 +0200 Subject: [PATCH 11/22] fix: add version check --- .../src/native/nativecsharpextension.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp b/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp index e0a00aa4..2b7748af 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp @@ -373,6 +373,15 @@ SQLRETURN SetHostCallbacks( return SQL_ERROR; } + // Validate the struct version before reading any version-gated fields. + // + if (hostCallbacks->Version < SQLEXTENSION_HOST_CALLBACKS_VERSION_1 || + hostCallbacks->Version > SQLEXTENSION_HOST_CALLBACKS_VERSION_1) + { + LOG_ERROR("SetHostCallbacks called with unsupported host callbacks version"); + return SQL_ERROR; + } + g_hostCallbacks = hostCallbacks; return g_dotnet_runtime->call_managed_method( From 80a707662497580d8c1744f9705028cbd2bda009 Mon Sep 17 00:00:00 2001 From: Miljan Veljovic Date: Thu, 28 May 2026 17:33:04 +0200 Subject: [PATCH 12/22] fix: address code review comments --- extension-host/include/sqlexternallanguage.h | 9 +++------ .../dotnet-core-CSharp/include/nativecsharpextension.h | 6 +++--- .../dotnet-core-CSharp/src/managed/utils/Logging.cs | 7 +++++-- .../src/native/nativecsharpextension.cpp | 4 ++-- .../test/src/native/CSharpSetHostCallbacksTests.cpp | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/extension-host/include/sqlexternallanguage.h b/extension-host/include/sqlexternallanguage.h index 932ea2c4..0d964338 100644 --- a/extension-host/include/sqlexternallanguage.h +++ b/extension-host/include/sqlexternallanguage.h @@ -150,9 +150,7 @@ enum ExtensionTraceLevel Extension_Verbose = 5 }; -// Callback function type for logging XEvents from an extension. -// The extension calls this function pointer (implemented by ExtHost) -// to fire XEvents from within the extension's code. +// Callback function provided by the host for logging XEvents from Extension. // typedef void (*PFunc_ExtensionLogXEvent)( const SQLCHAR *ExtensionName, @@ -165,8 +163,7 @@ typedef void (*PFunc_ExtensionLogXEvent)( SQLULEN MessageLength ); -// Host callbacks structure passed from ExtHost to the extension. -// The Version field allows future expansion without a new API version. +// Host callbacks structure passed from host to Extension.. // #define SQLEXTENSION_HOST_CALLBACKS_VERSION_1 1 @@ -177,7 +174,7 @@ typedef struct _SQLEXTENSION_HOST_CALLBACKS } SQLEXTENSION_HOST_CALLBACKS; // Optional API (v3+) -// Receives host callback functions from ExtHost. +// Receives host callback functions from the host. // SQLEXTENSION_INTERFACE SQLRETURN SetHostCallbacks( diff --git a/language-extensions/dotnet-core-CSharp/include/nativecsharpextension.h b/language-extensions/dotnet-core-CSharp/include/nativecsharpextension.h index 6afc7ee3..b1b4fdbb 100644 --- a/language-extensions/dotnet-core-CSharp/include/nativecsharpextension.h +++ b/language-extensions/dotnet-core-CSharp/include/nativecsharpextension.h @@ -142,8 +142,8 @@ SQLEXTENSION_INTERFACE SQLRETURN CleanupSession(SQLGUID sessionId, SQLUSMALLINT // SQLEXTENSION_INTERFACE SQLRETURN Cleanup(); -// Receives host callback function pointers from ExtHost. -// Optional API, supported since v3 of the extension API. +// Receives host callback function pointers from the host. +// Optional API, supported since v3 of the Extension API. // SQLEXTENSION_INTERFACE SQLRETURN SetHostCallbacks( SQLEXTENSION_HOST_CALLBACKS *hostCallbacks); @@ -152,6 +152,6 @@ SQLEXTENSION_INTERFACE SQLRETURN SetHostCallbacks( // static DotnetEnvironment* g_dotnet_runtime = nullptr; -// Host callbacks pointer provided by ExtHost via SetHostCallbacks. +// Host callbacks pointer provided by the host via SetHostCallbacks. // static SQLEXTENSION_HOST_CALLBACKS* g_hostCallbacks = nullptr; \ No newline at end of file diff --git a/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs b/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs index fa098dab..732f4237 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs @@ -107,6 +107,9 @@ public enum TraceLevel : ushort Verbose = 5, } + /// + /// Name of the Extension to be used for XEvent logging. + /// private const string ExtensionName = "CSharp"; /// @@ -115,8 +118,8 @@ public enum TraceLevel : ushort /// /// Session GUID. /// Task identifier. - /// Trace level (severity). - /// Error code (0 for informational). + /// Trace severity. + /// Error code for non-informational logs. /// The message to log. public static unsafe void LogXEvent( Guid sessionId, diff --git a/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp b/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp index 2b7748af..f8d82b87 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp @@ -348,9 +348,9 @@ SQLRETURN Cleanup() // Name: SetHostCallbacks // // Description: -// Receives a pointer to the host callbacks structure from ExtHost. +// Receives a pointer to the host callbacks structure. // Stores the pointer natively and forwards to managed code so the -// managed layer can call back into the host (e.g. for XEvent logging). +// managed layer can call back into the host. // // Returns: // SQL_SUCCESS on success, else SQL_ERROR diff --git a/language-extensions/dotnet-core-CSharp/test/src/native/CSharpSetHostCallbacksTests.cpp b/language-extensions/dotnet-core-CSharp/test/src/native/CSharpSetHostCallbacksTests.cpp index ff4a85ed..ee0bc300 100644 --- a/language-extensions/dotnet-core-CSharp/test/src/native/CSharpSetHostCallbacksTests.cpp +++ b/language-extensions/dotnet-core-CSharp/test/src/native/CSharpSetHostCallbacksTests.cpp @@ -8,7 +8,7 @@ // Tests the optional SetHostCallbacks API exported by the .NET Core C# // extension. Verifies symbol resolution, null-pointer handling, and that // a non-null LogXEvent callback supplied via SQLEXTENSION_HOST_CALLBACKS -// is invoked by managed code (callback registration + forwarding). +// is invoked by managed code. // //********************************************************************* #include "CSharpExtensionApiTests.h" From 44b3f23fd47cd17ba21c82bf54cc4f9207ebdf5d Mon Sep 17 00:00:00 2001 From: Miljan Veljovic Date: Thu, 28 May 2026 23:25:13 +0200 Subject: [PATCH 13/22] fix: address code review comments --- extension-host/include/sqlexternallanguage.h | 4 +- .../include/nativecsharpextension.h | 7 ++-- .../src/managed/utils/Logging.cs | 39 +++++++++++++++---- .../src/native/nativecsharpextension.cpp | 5 +++ 4 files changed, 43 insertions(+), 12 deletions(-) diff --git a/extension-host/include/sqlexternallanguage.h b/extension-host/include/sqlexternallanguage.h index 0d964338..4e8c9bcb 100644 --- a/extension-host/include/sqlexternallanguage.h +++ b/extension-host/include/sqlexternallanguage.h @@ -137,7 +137,7 @@ SQLRETURN CleanupSession( SQLEXTENSION_INTERFACE SQLRETURN Cleanup(); -// Trace levels for events logged from via LogXEvent function. +// Trace levels for events logged via LogXEvent function. // Lowest numeric value is the most severe, matching the Windows ETW // TRACE_LEVEL_* convention. // @@ -163,7 +163,7 @@ typedef void (*PFunc_ExtensionLogXEvent)( SQLULEN MessageLength ); -// Host callbacks structure passed from host to Extension.. +// Host callbacks structure passed from host to Extension. // #define SQLEXTENSION_HOST_CALLBACKS_VERSION_1 1 diff --git a/language-extensions/dotnet-core-CSharp/include/nativecsharpextension.h b/language-extensions/dotnet-core-CSharp/include/nativecsharpextension.h index b1b4fdbb..2e450177 100644 --- a/language-extensions/dotnet-core-CSharp/include/nativecsharpextension.h +++ b/language-extensions/dotnet-core-CSharp/include/nativecsharpextension.h @@ -148,10 +148,11 @@ SQLEXTENSION_INTERFACE SQLRETURN Cleanup(); SQLEXTENSION_INTERFACE SQLRETURN SetHostCallbacks( SQLEXTENSION_HOST_CALLBACKS *hostCallbacks); -// Dotnet environment pointer +// Dotnet environment pointer. Defined in nativecsharpextension.cpp. // -static DotnetEnvironment* g_dotnet_runtime = nullptr; +extern DotnetEnvironment* g_dotnet_runtime; // Host callbacks pointer provided by the host via SetHostCallbacks. +// Defined in nativecsharpextension.cpp. // -static SQLEXTENSION_HOST_CALLBACKS* g_hostCallbacks = nullptr; \ No newline at end of file +extern SQLEXTENSION_HOST_CALLBACKS* g_hostCallbacks; \ No newline at end of file diff --git a/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs b/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs index 732f4237..22576067 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs @@ -112,6 +112,13 @@ public enum TraceLevel : ushort /// private const string ExtensionName = "CSharp"; + /// + /// Cached UTF-8 bytes of , allocated once + /// to avoid re-encoding on every LogXEvent call. Always non-empty, + /// so it does not need the zero-length sentinel guard. + /// + private static readonly byte[] s_utf8ExtNameBytes = Encoding.UTF8.GetBytes(ExtensionName); + /// /// Logs a message through the host's XEvent infrastructure. /// If no host callback is registered, this is a no-op. @@ -129,22 +136,40 @@ public static unsafe void LogXEvent( string message) { if (_logXEventCallback == null) + { return; + } + + // Ensure message is not null to avoid issues during UTF-8 encoding. + string safeMessage = message ?? string.Empty; + + // Convert the message to a UTF-8 byte array for native interop. + byte[] utf8MessageBytes = Encoding.UTF8.GetBytes(safeMessage); + + // Capture the real byte length. + ulong messageLen = (ulong)utf8MessageBytes.Length; + + // As `fixed` on a zero-length array yields a null pointer, + // validate that the byte array is not empty. + // + if (utf8MessageBytes.Length == 0) + { + utf8MessageBytes = new byte[] { 0 }; + } - byte[] utf8ExtName = Encoding.UTF8.GetBytes(ExtensionName); - byte[] utf8Bytes = Encoding.UTF8.GetBytes(message ?? string.Empty); - fixed (byte* pExtName = utf8ExtName) - fixed (byte* pBytes = utf8Bytes) + // Call the host's LogXEvent callback with the prepared parameters. + fixed (byte* pExtName = s_utf8ExtNameBytes) + fixed (byte* pMessage = utf8MessageBytes) { _logXEventCallback( (char*)pExtName, - (ulong)utf8ExtName.Length, + (ulong)s_utf8ExtNameBytes.Length, sessionId, taskId, (ushort)traceLevel, errorCode, - (char*)pBytes, - (ulong)utf8Bytes.Length); + (char*)pMessage, + messageLen); } } diff --git a/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp b/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp index f8d82b87..e371bdca 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp @@ -13,6 +13,11 @@ #define nameof(x) #x +// Single definitions for the globals declared (extern) in the header. +// +DotnetEnvironment* g_dotnet_runtime = nullptr; +SQLEXTENSION_HOST_CALLBACKS* g_hostCallbacks = nullptr; + //-------------------------------------------------------------------------------------------------- // Name: UTF8PtrToStr // From 87b86366737f5007f1e6a0d6e555d6106ef533c2 Mon Sep 17 00:00:00 2001 From: Miljan Veljovic Date: Thu, 28 May 2026 23:31:06 +0200 Subject: [PATCH 14/22] fix: avoid race condition on concurrent cleanups --- .../src/managed/utils/Logging.cs | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs b/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs index 22576067..c336d2aa 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs @@ -32,8 +32,10 @@ class Logging /// /// Host-provided LogXEvent callback, set via SetHostCallbacks. + /// Marked volatile so concurrent readers in LogXEvent always + /// observe the latest write from SetLogXEventCallback. /// - private static CSharpExtension.LogXEventCallbackDelegate _logXEventCallback; + private static volatile CSharpExtension.LogXEventCallbackDelegate _logXEventCallback; /// /// Static constructor to initialize the custom text writers for stdout and stderr. @@ -135,7 +137,11 @@ public static unsafe void LogXEvent( int errorCode, string message) { - if (_logXEventCallback == null) + // Snapshot the callback once so a concurrent cleanups between the null-check and the invocation + // cannot turn this into a NullReferenceException. + // + CSharpExtension.LogXEventCallbackDelegate callback = _logXEventCallback; + if (callback == null) { return; } @@ -158,18 +164,25 @@ public static unsafe void LogXEvent( } // Call the host's LogXEvent callback with the prepared parameters. - fixed (byte* pExtName = s_utf8ExtNameBytes) - fixed (byte* pMessage = utf8MessageBytes) + try + { + fixed (byte* pExtName = s_utf8ExtNameBytes) + fixed (byte* pMessage = utf8MessageBytes) + { + callback( + (char*)pExtName, + (ulong)s_utf8ExtNameBytes.Length, + sessionId, + taskId, + (ushort)traceLevel, + errorCode, + (char*)pMessage, + messageLen); + } + } + catch (Exception ex) { - _logXEventCallback( - (char*)pExtName, - (ulong)s_utf8ExtNameBytes.Length, - sessionId, - taskId, - (ushort)traceLevel, - errorCode, - (char*)pMessage, - messageLen); + Debug.WriteLine($"LogXEvent host callback threw: {ex}"); } } From dfc2d4dd048a7f38c99beabe76516c2fe462cd62 Mon Sep 17 00:00:00 2001 From: Miljan Veljovic Date: Thu, 28 May 2026 23:32:58 +0200 Subject: [PATCH 15/22] fix: type mismatch --- .../dotnet-core-CSharp/src/managed/CSharpExtension.cs | 4 ++-- .../dotnet-core-CSharp/src/managed/utils/Logging.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs b/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs index 914d76c8..405f8808 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs @@ -606,13 +606,13 @@ public static short GetOutputParam( /// [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void LogXEventCallbackDelegate( - char *extensionName, + byte *extensionName, ulong extensionNameLength, Guid sessionId, ushort taskId, ushort traceLevel, int errorCode, - char *message, + byte *message, ulong messageLength); /// diff --git a/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs b/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs index c336d2aa..cbf9f451 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs @@ -170,13 +170,13 @@ public static unsafe void LogXEvent( fixed (byte* pMessage = utf8MessageBytes) { callback( - (char*)pExtName, + pExtName, (ulong)s_utf8ExtNameBytes.Length, sessionId, taskId, (ushort)traceLevel, errorCode, - (char*)pMessage, + pMessage, messageLen); } } From 01d98893f2079b1f5e09b403002b7620a394361e Mon Sep 17 00:00:00 2001 From: Miljan Veljovic Date: Thu, 28 May 2026 23:46:19 +0200 Subject: [PATCH 16/22] fix: address race condition concerns --- extension-host/include/sqlexternallanguage.h | 1 + .../src/native/nativecsharpextension.cpp | 21 +++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/extension-host/include/sqlexternallanguage.h b/extension-host/include/sqlexternallanguage.h index 4e8c9bcb..009df3b0 100644 --- a/extension-host/include/sqlexternallanguage.h +++ b/extension-host/include/sqlexternallanguage.h @@ -166,6 +166,7 @@ typedef void (*PFunc_ExtensionLogXEvent)( // Host callbacks structure passed from host to Extension. // #define SQLEXTENSION_HOST_CALLBACKS_VERSION_1 1 +#define SQLEXTENSION_HOST_CALLBACKS_MAX_SUPPORTED_VERSION SQLEXTENSION_HOST_CALLBACKS_VERSION_1 typedef struct _SQLEXTENSION_HOST_CALLBACKS { diff --git a/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp b/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp index e371bdca..5fa961b5 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp @@ -343,7 +343,24 @@ SQLRETURN CleanupSession(SQLGUID sessionId, SQLUSMALLINT taskId) SQLRETURN Cleanup() { LOG("nativecsharpextension::Cleanup"); - g_hostCallbacks = nullptr; + + // Clear the managed-side callbacks delegates before tearing down the + // runtime so any thread that races into callbacks during shutdown + // cannot invoke a host function pointer whose backing implementation is + // about to be freed. + // + if (g_dotnet_runtime != nullptr) + { + SQLEXTENSION_HOST_CALLBACKS nullCallbacks = {}; + nullCallbacks.Version = SQLEXTENSION_HOST_CALLBACKS_VERSION_1; + nullCallbacks.LogXEvent = nullptr; + + g_dotnet_runtime->call_managed_method( + nameof(SetHostCallbacks), + &nullCallbacks); + } + + g_hostCallbacks = nullptr; delete g_dotnet_runtime; g_dotnet_runtime = nullptr; return SQL_SUCCESS; @@ -381,7 +398,7 @@ SQLRETURN SetHostCallbacks( // Validate the struct version before reading any version-gated fields. // if (hostCallbacks->Version < SQLEXTENSION_HOST_CALLBACKS_VERSION_1 || - hostCallbacks->Version > SQLEXTENSION_HOST_CALLBACKS_VERSION_1) + hostCallbacks->Version > SQLEXTENSION_HOST_CALLBACKS_MAX_SUPPORTED_VERSION) { LOG_ERROR("SetHostCallbacks called with unsupported host callbacks version"); return SQL_ERROR; From 456aa7807efc4e810a21d12332078a333d38eb37 Mon Sep 17 00:00:00 2001 From: Miljan Veljovic Date: Thu, 28 May 2026 23:50:47 +0200 Subject: [PATCH 17/22] fix: avoid dangling pointer of host callbacks --- .../src/native/nativecsharpextension.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp b/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp index 5fa961b5..6721f188 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp @@ -18,6 +18,13 @@ DotnetEnvironment* g_dotnet_runtime = nullptr; SQLEXTENSION_HOST_CALLBACKS* g_hostCallbacks = nullptr; +// g_hostCallbacks points at internal copy of the host's callbacks +// struct g_hostCallbacksCopy rather than the caller-owned memory, so the +// pointer can never dangle if the host passed a stack-allocated struct to +// SetHostCallbacks. +// +static SQLEXTENSION_HOST_CALLBACKS g_hostCallbacksCopy = {}; + //-------------------------------------------------------------------------------------------------- // Name: UTF8PtrToStr // @@ -404,7 +411,11 @@ SQLRETURN SetHostCallbacks( return SQL_ERROR; } - g_hostCallbacks = hostCallbacks; + // Take a shallow copy of the caller's struct so g_hostCallbacks cannot + // dangle if the host passed a stack-allocated SQLEXTENSION_HOST_CALLBACKS. + // + g_hostCallbacksCopy = *hostCallbacks; + g_hostCallbacks = &g_hostCallbacksCopy; return g_dotnet_runtime->call_managed_method( nameof(SetHostCallbacks), From 011520de8fc2e24880e8ee52a5761e962839033d Mon Sep 17 00:00:00 2001 From: Miljan Veljovic Date: Fri, 29 May 2026 01:20:08 +0200 Subject: [PATCH 18/22] feat: extract trace level into separate header file --- .../include/sqlextensionhostcallbacks.h | 69 +++++++++++++++++++ extension-host/include/sqlexternallanguage.h | 45 ------------ .../include/nativecsharpextension.h | 1 + 3 files changed, 70 insertions(+), 45 deletions(-) create mode 100644 extension-host/include/sqlextensionhostcallbacks.h diff --git a/extension-host/include/sqlextensionhostcallbacks.h b/extension-host/include/sqlextensionhostcallbacks.h new file mode 100644 index 00000000..500d593e --- /dev/null +++ b/extension-host/include/sqlextensionhostcallbacks.h @@ -0,0 +1,69 @@ +//********************************************************************* +// Copyright (c) Microsoft Corporation. +// +// Host callbacks protocol between Exthost and a 3rd party extension. +// Optional API surface introduced alongside v3 of the Extension API. +// +// @File: sqlextensionhostcallbacks.h +// +//********************************************************************* + +#ifndef __SQLEXTENSIONHOSTCALLBACKS +#define __SQLEXTENSIONHOSTCALLBACKS + +#include "sqlexternallanguage.h" + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +// Trace levels for events logged via LogXEvent function. +// Lowest numeric value is the most severe, matching the Windows ETW +// TRACE_LEVEL_* convention. +// +enum ExtensionTraceLevel +{ + Extension_Critical = 1, + Extension_Error = 2, + Extension_Warning = 3, + Extension_Information = 4, + Extension_Verbose = 5 +}; + +// Callback function provided by the host for logging XEvents from Extension. +// +typedef void (*PFunc_ExtensionLogXEvent)( + const SQLCHAR *ExtensionName, + SQLULEN ExtensionNameLength, + SQLGUID SessionId, + SQLUSMALLINT TaskId, + SQLUSMALLINT TraceLevel, + SQLINTEGER ErrorCode, + const SQLCHAR *Message, + SQLULEN MessageLength + ); + +// Host callbacks structure passed from host to Extension. +// +#define SQLEXTENSION_HOST_CALLBACKS_VERSION_1 1 +#define SQLEXTENSION_HOST_CALLBACKS_MAX_SUPPORTED_VERSION SQLEXTENSION_HOST_CALLBACKS_VERSION_1 + +typedef struct _SQLEXTENSION_HOST_CALLBACKS +{ + SQLUSMALLINT Version; + PFunc_ExtensionLogXEvent LogXEvent; +} SQLEXTENSION_HOST_CALLBACKS; + +// Optional API (v3+) +// Receives host callback functions from the host. +// +SQLEXTENSION_INTERFACE +SQLRETURN SetHostCallbacks( + SQLEXTENSION_HOST_CALLBACKS *Callbacks + ); + +#ifdef __cplusplus +} /* End of extern "C" { */ +#endif/* __cplusplus */ + +#endif/* __SQLEXTENSIONHOSTCALLBACKS */ diff --git a/extension-host/include/sqlexternallanguage.h b/extension-host/include/sqlexternallanguage.h index 009df3b0..57f3df24 100644 --- a/extension-host/include/sqlexternallanguage.h +++ b/extension-host/include/sqlexternallanguage.h @@ -137,51 +137,6 @@ SQLRETURN CleanupSession( SQLEXTENSION_INTERFACE SQLRETURN Cleanup(); -// Trace levels for events logged via LogXEvent function. -// Lowest numeric value is the most severe, matching the Windows ETW -// TRACE_LEVEL_* convention. -// -enum ExtensionTraceLevel -{ - Extension_Critical = 1, - Extension_Error = 2, - Extension_Warning = 3, - Extension_Information = 4, - Extension_Verbose = 5 -}; - -// Callback function provided by the host for logging XEvents from Extension. -// -typedef void (*PFunc_ExtensionLogXEvent)( - const SQLCHAR *ExtensionName, - SQLULEN ExtensionNameLength, - SQLGUID SessionId, - SQLUSMALLINT TaskId, - SQLUSMALLINT TraceLevel, - SQLINTEGER ErrorCode, - const SQLCHAR *Message, - SQLULEN MessageLength - ); - -// Host callbacks structure passed from host to Extension. -// -#define SQLEXTENSION_HOST_CALLBACKS_VERSION_1 1 -#define SQLEXTENSION_HOST_CALLBACKS_MAX_SUPPORTED_VERSION SQLEXTENSION_HOST_CALLBACKS_VERSION_1 - -typedef struct _SQLEXTENSION_HOST_CALLBACKS -{ - SQLUSMALLINT Version; - PFunc_ExtensionLogXEvent LogXEvent; -} SQLEXTENSION_HOST_CALLBACKS; - -// Optional API (v3+) -// Receives host callback functions from the host. -// -SQLEXTENSION_INTERFACE -SQLRETURN SetHostCallbacks( - SQLEXTENSION_HOST_CALLBACKS *Callbacks - ); - #ifdef __cplusplus } /* End of extern "C" { */ #endif/* __cplusplus */ diff --git a/language-extensions/dotnet-core-CSharp/include/nativecsharpextension.h b/language-extensions/dotnet-core-CSharp/include/nativecsharpextension.h index 2e450177..15872d79 100644 --- a/language-extensions/dotnet-core-CSharp/include/nativecsharpextension.h +++ b/language-extensions/dotnet-core-CSharp/include/nativecsharpextension.h @@ -24,6 +24,7 @@ #include #include "sqlexternallanguage.h" #include "sqlexternallibrary.h" +#include "sqlextensionhostcallbacks.h" #include "DotnetEnvironment.h" // Returns the string with the address of the first character and length From e726883b267ca1569f34cc292db1e5a0dc90ff77 Mon Sep 17 00:00:00 2001 From: Miljan Veljovic Date: Fri, 29 May 2026 13:15:36 +0200 Subject: [PATCH 19/22] fix: extract trace level enum in a separate header file --- .../include/sqlextensionhostcallbacks.h | 38 ++++++++++++------- .../include/sqlextensiontracelevel.h | 26 +++++++++++++ 2 files changed, 50 insertions(+), 14 deletions(-) create mode 100644 extension-host/include/sqlextensiontracelevel.h diff --git a/extension-host/include/sqlextensionhostcallbacks.h b/extension-host/include/sqlextensionhostcallbacks.h index 500d593e..ce53260a 100644 --- a/extension-host/include/sqlextensionhostcallbacks.h +++ b/extension-host/include/sqlextensionhostcallbacks.h @@ -1,7 +1,7 @@ //********************************************************************* // Copyright (c) Microsoft Corporation. // -// Host callbacks protocol between Exthost and a 3rd party extension. +// Host callbacks protocol between the host and extensions. // Optional API surface introduced alongside v3 of the Extension API. // // @File: sqlextensionhostcallbacks.h @@ -12,24 +12,12 @@ #define __SQLEXTENSIONHOSTCALLBACKS #include "sqlexternallanguage.h" +#include "sqlextensiontracelevel.h" #ifdef __cplusplus extern "C" { #endif /* __cplusplus */ -// Trace levels for events logged via LogXEvent function. -// Lowest numeric value is the most severe, matching the Windows ETW -// TRACE_LEVEL_* convention. -// -enum ExtensionTraceLevel -{ - Extension_Critical = 1, - Extension_Error = 2, - Extension_Warning = 3, - Extension_Information = 4, - Extension_Verbose = 5 -}; - // Callback function provided by the host for logging XEvents from Extension. // typedef void (*PFunc_ExtensionLogXEvent)( @@ -50,8 +38,30 @@ typedef void (*PFunc_ExtensionLogXEvent)( typedef struct _SQLEXTENSION_HOST_CALLBACKS { + // Highest SQLEXTENSION_HOST_CALLBACKS_VERSION_* the host populates. + // Extension must validate before reading version-gated fields. + // SQLUSMALLINT Version; + + // Explicit padding so SizeInBytes is naturally 4-byte aligned regardless + // of compiler packing settings. Must be zero. + // + SQLUSMALLINT Reserved0; + + // sizeof(SQLEXTENSION_HOST_CALLBACKS) as the host saw it at build time. + // Extension must validate this is greater or equal the size of every field it intends + // to read. Lets a newer Extension safely run against an older host that supplied a smaller struct. + // + SQLUINTEGER SizeInBytes; + + // Version 1 callbacks. + // PFunc_ExtensionLogXEvent LogXEvent; + + // Reserved for future expansion. Zero-initialized by the host. Extension must not read or call these. + // + void *Reserved1; + void *Reserved2; } SQLEXTENSION_HOST_CALLBACKS; // Optional API (v3+) diff --git a/extension-host/include/sqlextensiontracelevel.h b/extension-host/include/sqlextensiontracelevel.h new file mode 100644 index 00000000..0afdcd01 --- /dev/null +++ b/extension-host/include/sqlextensiontracelevel.h @@ -0,0 +1,26 @@ +//********************************************************************* +// Copyright (c) Microsoft Corporation. +// +// ExtensionTraceLevel enum shared between the host and extensions. +// +// @File: sqlextensiontracelevel.h +// +//********************************************************************* + +#ifndef __SQLEXTENSIONTRACELEVEL +#define __SQLEXTENSIONTRACELEVEL + +// Trace levels for events logged via the LogXEvent host callback. +// Lowest numeric value is the most severe, matching the Windows ETW +// TRACE_LEVEL_* convention. +// +enum ExtensionTraceLevel +{ + Extension_Critical = 1, + Extension_Error = 2, + Extension_Warning = 3, + Extension_Information = 4, + Extension_Verbose = 5 +}; + +#endif /* __SQLEXTENSIONTRACELEVEL */ From 74d7a5b9c1ecec464d1b9260784d2247612779d6 Mon Sep 17 00:00:00 2001 From: Miljan Veljovic Date: Fri, 29 May 2026 13:41:58 +0200 Subject: [PATCH 20/22] fix: use caller-supplied extension name, if provided, or default extension name otherwise --- .../src/managed/CSharpExtension.cs | 1 + .../src/managed/utils/Logging.cs | 27 +++++++++++-------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs b/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs index 405f8808..70c8215e 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs @@ -682,6 +682,7 @@ public static short SetHostCallbacks( Logging.SetLogXEventCallback(logXEvent); Logging.LogXEvent( + extensionName: null, Guid.Empty, taskId: 0, traceLevel: Logging.TraceLevel.Information, diff --git a/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs b/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs index cbf9f451..69556e00 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs @@ -110,27 +110,23 @@ public enum TraceLevel : ushort } /// - /// Name of the Extension to be used for XEvent logging. + /// Default name of the Extension to be used for XEvent logging when + /// the caller does not supply one. /// - private const string ExtensionName = "CSharp"; - - /// - /// Cached UTF-8 bytes of , allocated once - /// to avoid re-encoding on every LogXEvent call. Always non-empty, - /// so it does not need the zero-length sentinel guard. - /// - private static readonly byte[] s_utf8ExtNameBytes = Encoding.UTF8.GetBytes(ExtensionName); + private const string DefaultExtensionName = "CSharp"; /// /// Logs a message through the host's XEvent infrastructure. /// If no host callback is registered, this is a no-op. /// + /// Extension name. /// Session GUID. /// Task identifier. /// Trace severity. /// Error code for non-informational logs. /// The message to log. public static unsafe void LogXEvent( + string extensionName, Guid sessionId, ushort taskId, TraceLevel traceLevel, @@ -163,15 +159,24 @@ public static unsafe void LogXEvent( utf8MessageBytes = new byte[] { 0 }; } + // Use the caller-supplied Extension name when non-empty, + // otherwise fall back to the default value. + // + string safeExtensionName = string.IsNullOrEmpty(extensionName) + ? DefaultExtensionName + : extensionName; + byte[] utf8ExtNameBytes = Encoding.UTF8.GetBytes(safeExtensionName); + ulong extNameLen = (ulong)utf8ExtNameBytes.Length; + // Call the host's LogXEvent callback with the prepared parameters. try { - fixed (byte* pExtName = s_utf8ExtNameBytes) + fixed (byte* pExtName = utf8ExtNameBytes) fixed (byte* pMessage = utf8MessageBytes) { callback( pExtName, - (ulong)s_utf8ExtNameBytes.Length, + extNameLen, sessionId, taskId, (ushort)traceLevel, From b5c7417bd2decccf8238e6afe3fe86361e22348e Mon Sep 17 00:00:00 2001 From: Miljan Veljovic Date: Fri, 29 May 2026 13:54:27 +0200 Subject: [PATCH 21/22] fix: forward compatibility on host callback versions --- .../src/managed/CSharpExtension.cs | 17 +++++++++-------- .../src/managed/utils/Logging.cs | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs b/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs index 70c8215e..6ddf73be 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs @@ -627,10 +627,12 @@ public struct SqlExtensionHostCallbacks } /// - /// Highest SQLEXTENSION_HOST_CALLBACKS version this extension understands. - /// Must match SQLEXTENSION_HOST_CALLBACKS_VERSION_* in sqlexternallanguage.h. + /// Minimal SQLEXTENSION_HOST_CALLBACKS version this extension understands. + /// If host callbacks version is greater than this, extension will still parse and read + /// known host callbacks and ignore unknown fields, allowing forward compatibility. /// - private const ushort MaxSupportedHostCallbacksVersion = 1; + /// + private const ushort MinSupportedHostCallbacksVersion = 1; /// /// This delegate declares the delegate type of SetHostCallbacks. @@ -640,12 +642,12 @@ public delegate short SetHostCallbacksDelegate( /// /// This method implements SetHostCallbacks API. - /// Receives a pointer to the host callbacks structure and stores the - /// callback function pointers so managed code can call back into the host. + /// Receives a pointer to the host callbacks structure, reads the callback + /// function pointers during this call, and stores any needed managed + /// delegates so managed code can call back into the host. /// /// /// Pointer to the SQLEXTENSION_HOST_CALLBACKS structure provided by the host. - /// The host owns this memory and keeps it alive for the extension's lifetime. /// /// /// SQL_SUCCESS(0), SQL_ERROR(-1) @@ -663,8 +665,7 @@ public static short SetHostCallbacks( // Validate the struct version before reading any version-gated fields. // - if (hostCallbacks->Version == 0 || - hostCallbacks->Version > MaxSupportedHostCallbacksVersion) + if (hostCallbacks->Version < MinSupportedHostCallbacksVersion) { Logging.Error( "CSharpExtension::SetHostCallbacks: unsupported host callbacks version: " + diff --git a/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs b/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs index 69556e00..4dc95996 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs @@ -133,7 +133,7 @@ public static unsafe void LogXEvent( int errorCode, string message) { - // Snapshot the callback once so a concurrent cleanups between the null-check and the invocation + // Snapshot the callback once so a concurrent cleanup between the null-check and the invocation // cannot turn this into a NullReferenceException. // CSharpExtension.LogXEventCallbackDelegate callback = _logXEventCallback; From 2f9a8c9ce0a1f58a07aa3a643b98991992f5fbe5 Mon Sep 17 00:00:00 2001 From: Miljan Veljovic Date: Fri, 29 May 2026 14:41:20 +0200 Subject: [PATCH 22/22] fix: forward compatibility test --- .../include/sqlextensionhostcallbacks.h | 2 +- .../src/native/nativecsharpextension.cpp | 3 +- .../native/CSharpSetHostCallbacksTests.cpp | 64 +++++++++++++++++++ 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/extension-host/include/sqlextensionhostcallbacks.h b/extension-host/include/sqlextensionhostcallbacks.h index ce53260a..d0fbf7a1 100644 --- a/extension-host/include/sqlextensionhostcallbacks.h +++ b/extension-host/include/sqlextensionhostcallbacks.h @@ -34,7 +34,7 @@ typedef void (*PFunc_ExtensionLogXEvent)( // Host callbacks structure passed from host to Extension. // #define SQLEXTENSION_HOST_CALLBACKS_VERSION_1 1 -#define SQLEXTENSION_HOST_CALLBACKS_MAX_SUPPORTED_VERSION SQLEXTENSION_HOST_CALLBACKS_VERSION_1 +#define SQLEXTENSION_HOST_CALLBACKS_MIN_SUPPORTED_VERSION SQLEXTENSION_HOST_CALLBACKS_VERSION_1 typedef struct _SQLEXTENSION_HOST_CALLBACKS { diff --git a/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp b/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp index 6721f188..c501234d 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp @@ -404,8 +404,7 @@ SQLRETURN SetHostCallbacks( // Validate the struct version before reading any version-gated fields. // - if (hostCallbacks->Version < SQLEXTENSION_HOST_CALLBACKS_VERSION_1 || - hostCallbacks->Version > SQLEXTENSION_HOST_CALLBACKS_MAX_SUPPORTED_VERSION) + if (hostCallbacks->Version < SQLEXTENSION_HOST_CALLBACKS_MIN_SUPPORTED_VERSION) { LOG_ERROR("SetHostCallbacks called with unsupported host callbacks version"); return SQL_ERROR; diff --git a/language-extensions/dotnet-core-CSharp/test/src/native/CSharpSetHostCallbacksTests.cpp b/language-extensions/dotnet-core-CSharp/test/src/native/CSharpSetHostCallbacksTests.cpp index ee0bc300..c2968d51 100644 --- a/language-extensions/dotnet-core-CSharp/test/src/native/CSharpSetHostCallbacksTests.cpp +++ b/language-extensions/dotnet-core-CSharp/test/src/native/CSharpSetHostCallbacksTests.cpp @@ -164,4 +164,68 @@ namespace ExtensionApiTest EXPECT_EQ(rc, SQL_SUCCESS); EXPECT_TRUE(g_capturedLogEvents.empty()); } + + //---------------------------------------------------------------------------------------------- + // Name: SetHostCallbacks_FutureVersionIsAccepted + // + // Description: + // A host that advertises a newer SQLEXTENSION_HOST_CALLBACKS version than + // the Extension was built against must still be accepted, preserving forward compatibility. + // The Extension is expected to read only the fields it and ignore the rest. + // + TEST_F(CSharpExtensionApiTests, SetHostCallbacks_FutureVersionIsAccepted) + { + FN_setHostCallbacks *fn = RESOLVE_SET_HOST_CALLBACKS(); + ASSERT_NE(fn, nullptr); + + g_capturedLogEvents.clear(); + + // Hypothetical "vNext" host callbacks struct. + // + typedef void (*PFunc_FutureCallback)(void); + struct FutureHostCallbacks + { + SQLUSMALLINT Version; + SQLUSMALLINT Reserved0; + SQLUINTEGER SizeInBytes; + PFunc_ExtensionLogXEvent LogXEvent; + void *Reserved1; + void *Reserved2; + PFunc_FutureCallback FutureCallback; + }; + + // Dummy v2 host callback. + // + static bool s_futureCallbackInvoked = false; + s_futureCallbackInvoked = false; + struct DummyFutureCallback + { + static void Invoke() { s_futureCallbackInvoked = true; } + }; + + // Populate rest of the fields in a host callbacks struct before invoking `SetHostCallbacks`. + // + FutureHostCallbacks hostCallbacks{}; + hostCallbacks.Version = SQLEXTENSION_HOST_CALLBACKS_VERSION_1 + 1; + hostCallbacks.SizeInBytes = sizeof(FutureHostCallbacks); + hostCallbacks.LogXEvent = &TestLogXEventCallback; + hostCallbacks.FutureCallback = &DummyFutureCallback::Invoke; + + SQLRETURN rc = fn(reinterpret_cast(&hostCallbacks)); + EXPECT_EQ(rc, SQL_SUCCESS) + << "Extension must accept newer host callback versions for " + "forward compatibility"; + + ASSERT_FALSE(g_capturedLogEvents.empty()) + << "Extension should still consume the known v1 LogXEvent slot " + "when host advertises a newer version"; + + const CapturedLogEvent &ev = g_capturedLogEvents.front(); + EXPECT_EQ(ev.extensionName, string("CSharp")); + EXPECT_EQ(ev.traceLevel, static_cast(Extension_Information)); + + EXPECT_FALSE(s_futureCallbackInvoked) + << "Extension built against v1 must not invoke unknown future " + "callbacks advertised by a newer host"; + } }