65.9K
CodeProject 正在变化。 阅读更多。
Home

C++ Quant 库的类型安全封装

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (9投票s)

2014年5月4日

CPOL

19分钟阅读

viewsIcon

18456

downloadIcon

298

使用高级技术为非线程安全、非类型安全的 API 提供高性能、并发、类型安全的 C# API

引言

从 C# 使用原生的 C++ 库从来都不是一件简单的事,而涉及量化金融的库更带来了其独特的挑战,部分原因是该领域的从业者(即“量化师”)的数学重点。

本文在 C# 和 C++ 语言、Windows 操作系统以及 Visual Studio 本身中采用了一些高级技术,提供了一个高性能、并发、类型安全的 C# API。

  • 介绍了一种面向 Variant 的设计,用于典型的原生、非线程安全、类 C 的量化库,包括基本(“香草”)期权的分析和数值定价(通过 Financial Numerical Recipes);
  • 一个单一的 C++ Translator DLL,以通用方式封装此库并翻译异常;
  • 以及一个非常类型安全、面向量化的 C# API,支持并发

演示的一些技术和工具包括:

  • 通过 XML 标记生成 API,利用 Visual Studio 内置的对 XSD 和 T4 模板的支持;
  • 支持跳过或默认参数,将“魔术”字符串映射到枚举,以及在 C# 中处理 C++ 联合体;
  • 使用通用的可 Dispose 结构体来消除所有装箱并最大限度地减少堆(GC)使用;
  • 直接运行时生成 P-Invoke 函子,包括手动 DLL 加载和解码;
  • 在 C# 中进行原生 Variant 内存处理,包括在原生调用之间进行类型安全的重用;
  • 使用不安全的 C# 代码实现高性能 C# Variant 包装器,用于直接进行 BSTR 和 SAFEARRAY 的编码/解码;
  • 使用 C++ 宏、可变参数模板和运行时遍历导出地址表来创建单一的原生 Translator DLL;
  • 高级使用 Windows Side by Side 清单来为非线程安全的原生库增加并发性

动机

本节展示了一个简单的示例,然后是实际的量化库风格调用。

第一个示例

给定 NativeAPI.DLL 中 C++ 函数 Function4 的签名如下:

//_Function4@16
extern "C" VARIANT __declspec(dllexport) __stdcall
Function4(VARIANT* _index, VARIANT* _arg1, VARIANT* _arg2, VARIANT* _arg3)

这通常是故意这样做的,以便提供一个直观的 VBA 声明,如下所示:

Declare Function Function4 Lib "NativeAPI" Alias "_Function4@16" _
(Index, Arg1, Optional Arg2, Optional Arg3)

不幸的是,显而易见的 P-Invoke 签名不受支持(截至 .NET v4.5)。

//1. This fails with a not supported error at run time
[DllImport("NativeAPI.DLL")]
extern static object Function4(
    ref object index, ref object first, 
    ref object second, ref object third);

因此,必须使用丑陋的名称修饰,并且所有参数都已移位,以暴露 stdcall 调用约定使用的隐藏返回参数。

//2. This works due to the stack layout of stdcall
[DllImport("NativeAPI.DLL", EntryPoint = "_Function4@16")]
extern static void Function4(
    out object result, 
    ref object index, ref object first, 
    ref object second, ref object third);

通过引用使用对象会使一切都变得不类型安全、效率低下且令人痛苦,因为 .NET 会将结构体、接口和类封送到其 COM 等效项,从而导致难以排查的运行时错误。

try
{
    object index = 1;
    object first = "first";
    object second = null;
    object third = "third";

    //This works: both inputs and output are untyped
    object res;
    Function4(out res, ref index, ref first, ref second, ref third);

更大的问题是,任何 C++ 异常都会被 .NET 吞噬并替换为相同的通用消息,丢失了量化库提供的关键错误信息。

     //This works but leaks the native C++ exception
    index = null;
    Function4_Works(out res, ref index, ref first, ref second, ref third);
}
catch (Exception ex)
{
    var msg = ex.Message;
    //C# only ever shows this message:
    //"System.Runtime.InteropServices.SEHException (0x80004005): 
    //External component has thrown an exception"

    //But the C++ error is really:
    //"Function4: index must be an integer"
}

第一个解决方案

使用 Visual Studio 内置的 XML 编辑支持,并受 XSD 约束来标记函数。

<function id="Function4" type="?Any">
   <arg id="Indexer" type="Integer"/>
   <arg id="Choice1" type="?Any"/>
   <arg id="Choice2" type="?Any"/>
   <arg id="Choice3" type="?Any"/>
</function>

然后可以高效地调用该函数并检索结果,原生 C++ 异常可选择性地以完全保真的方式重新抛出为 C# 异常。

using (var fn = NativeFunctions.Function4.New())
{
    //Set arguments
    fn.Indexer.Set(1);
    fn.Choice1.Set(1.23);
    fn.Choice2.Set("String");
    //Choice3 is optional!

    //And call
    NativeFunctions.Result choice = fn.Invoke();

此时的调试器视图很有启发性,准确地显示了 .NET 本身解释的原生 Variant 类型和值。

Function4 in Debugger

代码继续,展示了高效的重新调用和异常转换。

     //Efficiently deal with 'variant' results
    if (choice.Native.IsDouble)
    {
        double dbl = choice.GetDouble();
    }

    //Change one argument's value
    fn.Indexer.Set(3);
    //And call again
    choice = fn.Invoke();

    if (choice.Native.IsEmpty)
    {
        //Should be empty
    }

    //Force an error
    fn.Indexer.Set(-1);
    try
    {
        //And call again
        choice = fn.Invoke();
    } 
    catch (NativeFunctionException ex)
    {
        var msg = ex.Message;
        //Function4: index must be an integer
    }
}

复杂示例

给定 NativeAPI.DLL 中函数的 C++ 签名如下:

//namespace Native
#define API(FUNCTION) VARIANT __declspec(dllexport) __stdcall FUNCTION

API(CreateList)(VARIANT* _name, VARIANT* _list);

API(FunctionComplex)(VARIANT* _payOff, VARIANT* _frq, VARIANT* _overrides, 
VARIANT* _endDates, VARIANT* _roll, VARIANT* _ccy, VARIANT* _curve, 
VARIANT* _options, VARIANT* _schedule, VARIANT* _barrier);

然后必须使用丑陋的名称修饰,并且参数移位方式与之前相同。

[DllImport("NativeAPI.DLL", EntryPoint = "?CreateList@Native@@YG?AUtagVARIANT@@PAU2@0@Z")]
extern static void CreateList(out object result, ref object name, ref object values); 

[DllImport("NativeAPI.DLL", EntryPoint = "?FunctionComplex@Native@@YG?AUtagVARIANT@@PAU2@000000000@Z")]
extern static void FunctionComplex(out object result, ref object payoff, ref object frq, ref object overrides, 
    ref object endDates, ref object roll, ref object ccy, ref object curve, ref object options, 
    ref object schedule, ref object barrier);

首先,必须在量化库中将重载创建为一个命名列表。

object dummy;
object name = "Overrides";
//Note the shape of this array
object values = new object[,] 
{ 
    {"Amount", "PayDelay"},
    {0.9, 0.5}, //Amount
    {2, 2}      //PaymentLag
};
            
CreateList(out dummy, ref name, ref values);

假设计划存储在某处的类型安全结构中。

var schedule = new KeyStruct<DateTime, DateTime, decimal>[]
{
    KeyStruct.Create(DateTime.Now, DateTime.Now.AddMonths(1), 1.0m),
    KeyStruct.Create(DateTime.Now.AddMonths(2), DateTime.Now.AddMonths(3), 1.1m)
};

调用设置现在非常棘手,需要深入了解数组形状和魔术字符串,这些字符串由在该库工作的不同量化师定义,他们经常在不同时间工作。

object result;
//Magic strings
object payoff = "D";
object frq = "FourWeekly";
//Name of previously CreatedList prepended with '!'
object overrides = "!" + name;
//Note the array shape required; must remove times
object endDates = new object[,] { { DateTime.Now.Date, DateTime.Now.AddMonths(1).Date } };

//Can also be a string rule
object roll = 7;
object ccy = "AUD";
//Can also be the name of a pre-created curve
object curve = 0.1;

//More magic strings and fixed array shapes
object options = new object[,] { { "Extrapolate", true } };
//Note the array shape required; must remove times
object schedule2 = new object[,] { { DateTime.Now.Date, DateTime.Now.AddMonths(1).Date } };

//Must be very careful with shape (extra column) and casting here
var _barrier = new object[schedule.Length, 3 + 1];
for (int i=0; i<schedule.Length; i++)
{
    //Must remove any time component
    _barrier[i, 0] = schedule[i].Key.Date;
    _barrier[i, 1] = schedule[i].Value1.Date;
    //Must carefully cast decimal types
    _barrier[i, 2] = (double)schedule[i].Value2;
}
//Must always use local 'object' type for ref args
object barrier = _barrier;

FunctionComplex(out result, ref payoff, ref frq, ref overrides, ref endDates,
    ref roll, ref ccy, ref curve, ref options, ref schedule2, ref barrier);

处理结果同样令人痛苦,尤其是当类型不固定时。

//And now cast result
double res2 = (double)result;

复杂解决方案

函数再次被标记,但使用了更丰富的语法。

<create id="CreateList" type="List">
   <name/>
   <arg id="Data" type="Any" isArray="2d"/>
</create>

<function id="FunctionComplex" type="Double">
   <enum id="Payoff" type="QuantBarrierType"/>
   <argT id="PaymentFrequency" type="?EnumOrString" T="QuantFrequency"/>
   <argT id="Overrides" type="?NamedList" T="QuantOverrides"/>
   <arg id="EndDates" type="?Date" isArray="1d"/>
   <arg id="Roll" type="?StringOrNumber"/>
   <arg id="SettlementCcy" type="?String"/>
   <arg id="SettlementCurve" type="?Curve"/>
   <argT id="Options" type="?Map" T="QuantCurveOptions"/>
   <argT id="Schedule" type="Matrix" T="QuantAsianAveragingSchedule"/>
   <argT id="LowBarrier" type="?Matrix" T="QuantContinuousBarrierSchedule,
                                           QuantDiscreteBarrierSchedule"/>
</function>

再次先创建重载,但这次使用枚举并且没有装箱。

var overrides = QuantNamedList<QuantOverrides>.New("Overrides");
using (var bldr = overrides.NewBuilder(2, 2))
{
    bldr.Add(QuantOverrides.Amount).SetDoubleColumn(new[] { 0.9, 0.5 });
    bldr.Add(QuantOverrides.PaymentLag).SetIntegerColumn(new[] { 2, 2 });

    bldr.Invoke();
}

假设计划的类型安全结构相同,那么设置比以前容易得多,效率也高得多。请注意,如何直接设置结构体数组,而无需任何装箱或(无界)堆使用。

using (var fn = NativeFunctions.FunctionComplex.New())
{
    //Easy way if sourcing a simple collection
    fn.EndDates.Set(new[] { DateTime.Now, DateTime.Now.AddMonths(1) });

    var bldBarrier = fn.LowBarrier.SetWithBuilder1(2);
    //Optimal approach when translating an array of structures into individual arrays
    bldBarrier.StartDates.Set(schedule, t => t.Key);
    bldBarrier.EndDates.Set(schedule, t => t.Value1);
    bldBarrier.Levels.Set(schedule, t => (double)t.Value2);

支持通过 IDisposable 结构体进行类型化矩阵,以及枚举到字符串的自定义映射。

     using (var bldOptions = fn.Options.SetWithBuilder(1))
    {
        bldOptions.Add(QuantCurveOptions.Extrapolate, true);
    }
    fn.Payoff.Set(QuantBarrierType.Discrete);

字符串名称(包括与数字的联合体)的完全类型安全。

     //Use pre-set named map
    fn.Overrides.Set(overrides);
    fn.SettlementCurve.Set(0.1);

更多联合体,包括混合枚举、字符串和数字的联合体。

     fn.PaymentFrequency.Set(QuantFrequency.FourWeekly);
    fn.Roll.Set(7);

选择更简单或更高效但更复杂的方法。

     var bldAsian = fn.Schedule.SetWithBuilder(2);
    //Usual easiest approach:
    //bldAsian.Dates.Set(new[] { DateTime.Now, DateTime.Now.AddMonths(1) });
    //Optimal approach:
    bldAsian.Dates.SetAt(0, DateTime.Now);
    bldAsian.Dates.SetAt(1, DateTime.Now.AddMonths(1));

选择重新抛出异常或手动错误处理,以及结果的完全类型安全,没有任何不必要的堆使用(即使是嵌套数组也没有使用“object”)。

     fn.SettlementCcy.Set("AUD");

    double res = fn.Invoke();
}

此时的调试器视图同样具有启发性,准确显示了 .NET 本身解释的原生 Variant 类型和值。最重要的是,您可以看到传递给函数的实际值,例如 Payoff 的“D”,即使使用了枚举来设置它。

FunctionComplex in Debugger

使用简单波动率进行经典期权定价

从局部作用域开始,以显示如何跨函数调用重用 VARIANT 值:在本例中,是标的资产的定盘/观察值的名称。

 using (var scope = NativeFunctions.Scope())
{
    //Instead of local scoped variables can use a TypedVariantCache
    //for the long term storage of typed native values.
    var fixings = VariantFixings.New(scope);

现在将定盘/观察值加载到量化库中,分配一个任意名称,并启用 BSTR 的重用。请注意,该库期望一个类型化矩阵:第一列包含日期,最后一列包含相应的值。

     using (var fn = NativeFunctions.CreateFixings.New())
    {
        fn.AsOf.Set(DateTime.Now);
        fn.QuantName.Set("USDFixings");

        var builder = fn.DatesAndValues.SetWithBuilder(3);
        builder.Dates.Set(new[] { DateTime.Now, DateTime.Now.AddMonths(1), DateTime.Now.AddMonths(3) });
        builder.Numbers.Set(new[] { 1.1, 1.5, 1.9 });

        fn.Invoke();

        //Store for later - maintains typesafety
        fn.QuantName.Save(fixings);
    }

在无风险利率和波动率不变的情况下,对期权到期前三个月进行首次定价。同时计算一个风险度量(delta)。

     double pvAnalytic;
    using (var fn = NativeFunctions.ModelOptionBlackScholes.New(QuantMeasure.PV, QuantMeasure.Delta))
    {
        fn.AsOf.Set(DateTime.Now);
        fn.ExpiryDate.Set(DateTime.Now.AddMonths(3));
        fn.Payoff.Set(QuantCallOrPut.Call);
        fn.Rate.Set(0.15);  //15%
        fn.Spot.Set(1.1);
        fn.Strike.Set(1.0);
        fn.Vol.Set(0.20);   //20%

        var res = fn.Invoke();

结果是一个 VARIANT 向量,每个度量一个。通过了解度量类型,可以高效地解码,而无需任何装箱。

         //1. PV = scalar
        pvAnalytic = res.GetResult().GetDouble();
        //2. Delta = scalar
        var delta = res.GetResult().GetDouble();

现在可以直接在到期时重新定价期权,此时必须提供定盘/观察值。下面以完全类型安全的方式完成,该方式高效地重用了先前分配的 BSTR 作为名称。

         //On expiry day and fixed
        fn.AsOf.Set(DateTime.Now.AddMonths(3));
        //The cast must be explicit as this approach is not typesafe
        //fn.Fixings.Set((QuantFixings)"USDFixings");
        //The typesafe way is:
        fn.Fixings.SetFromShared(fixings);

        res = fn.Invoke();

结果同样易于高效访问,包括处理空情况。

         //Fixed at 1.9, less 1.0 strike, gives 0.9 payoff
        var pvExpired = res.GetResult().GetDouble();
        //No delta once fixed
        var bIsEmpty = res.IsEmpty; //True
        delta = res.GetResult().GetDouble(); //NaN
    }
}

使用复杂波动率进行数值期权定价

一个复杂的波动率过程在此表示为一个未命名(或命名)的映射,其中包含一个命名映射,类似于:

 "VolProcess" -> { 
                  { "VolatilityProcess", 0.20 }, 
                  /* other options */ 
                }; 
{
 {"Vol",      "!VolProcess" },
 { "VolType", "Flat"}
}

这个棘手的布局(包括命名映射)可以如下创建:

var vp = QuantVolProcess<QuantVolOptions>.New("VolProcess", 0.20); //20%
//Not using any additional options
vp.SetNoOptions();

然后像以前一样进行定价。

 double pvNumerical;
using (var fn = NativeFunctions.ModelOptionFD.New(QuantMeasure.PV, QuantMeasure.Delta))
{
    fn.AsOf.Set(DateTime.Now);
    fn.ExpiryDate.Set(DateTime.Now.AddMonths(3));
    fn.Payoff.Set(QuantCallOrPut.Call);
    fn.Rate.Set(0.15);  //15%
    fn.Spot.Set(1.1);
    fn.Strike.Set(1.0);
    fn.VolProcess.Set(vp);

    var res = fn.Invoke();

    //1. PV = scalar
    pvNumerical = res.GetResult().GetDouble();

    //2. Delta = scalar (not calculated by this model)
    var bIsEmpty = res.IsEmpty; //True
    var delta = res.GetResult().GetDouble(); //NaN

现在可以在重新计算之前设置控制数值算法的选项。

     //Now increase the steps
    var options = fn.Options.SetWithBuilder(2);
    options.Add(QuantFDModelOptions.SpotSteps, 100);
    options.Add(QuantFDModelOptions.TimeSteps, 100);
    //Alternative approach to a 'using' block:
    options.ThrowIfIncomplete();

    res = fn.Invoke();
    pvNumerical = res.GetResult().GetDouble();

}

var pvDiff = pvNumerical - pvAnalytic;

解决方案架构

示例 C++ 量化库设计分为主要的 NativeAPI.DLL 和依赖的 Utils.DLL。它故意以任何方式都不是线程安全的:特别是,使用了静态变量,因此期望这些 DLL 只有一个实例被加载到单个进程中并且所有对该库的调用都必须从单个线程发生。

这种设置在量化库中很常见,这些库主要为 Excel 2003 及更早版本编写;即,在 Microsoft 引入 XLL 的并发支持之前。如果库主要用于 VBA,而 VBA 不支持多线程,那么这种设置也很常见。

通过引入单一的 C++ Translator.DLL,可以自动枚举量化库 (NativeAPI.DLL) 的公共导出,并在运行时解码参数数量。然后,这允许将正确的包装器函数链到原始函数周围,以支持版本之间的 API 参数数量变化和异常转换。

然后可以通过引入 Windows SxS 清单文件并通过复制该文件以及二进制文件来实现并发,为每个所需的线程提供一个副本。

Overview Diagram

托管主题

讨论纯 C# 导向的设计和实现(高级互操作性在接下来的部分)。

XSD 约束 XML 编辑

Visual Studio 有一个不错的 XML 编辑器,在 XSD 的情况下可以提供建议、约束和错误检查。然而,其 XSD 编辑器在纯 XML 模式下效果最好。NativeFunctions.XSD 的开头部分如下:

<?xml version="1.0" encoding="utf-8"?>
<xs:schema id="NativeFunctions"
  targetNamespace="http://www.tovica.com/schemas/cp3/nativefunctions/1"
  elementFormDefault="qualified"
  xmlns="http://www.tovica.com/schemas/cp3/nativefunctions/1"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
>

<xs:simpleType name="PascalCaseString">
  <xs:restriction base="xs:string">
  <xs:pattern value="[A-Z][a-zA-Z0-9]*"/>
  </xs:restriction>
</xs:simpleType>

...

支持的数据类型是:

 <xs:simpleType name="Types">
<xs:restriction base="xs:string">
  <xs:enumeration value="Double"/>
  <xs:enumeration value="Date"/>
  <xs:enumeration value="DateTime"/>
  <xs:enumeration value="String"/>
  <xs:enumeration value="Integer"/>
  <xs:enumeration value="Boolean"/>
  <xs:enumeration value="Any"/>
<a name="900"></a>  <xs:enumeration value="Curve"/>
  <xs:enumeration value="Fixings"/>
  <xs:enumeration value="Surface"/>
  <xs:enumeration value="StringOrNumber"/>

  <xs:enumeration value="?Double"/>
  <xs:enumeration value="?Date"/>
  <xs:enumeration value="?DateTime"/>
  <xs:enumeration value="?String"/>
  <xs:enumeration value="?Integer"/>
  <xs:enumeration value="?Boolean"/>
  <xs:enumeration value="?Any"/>
  <xs:enumeration value="?Curve"/>
  <xs:enumeration value="?Fixings"/>
  <xs:enumeration value="?StringOrNumber"/>
</xs:restriction>
</xs:simpleType>

泛型也受支持,通常用于枚举或共享/重用 VARIANT。

 <xs:simpleType name="Generics">
<xs:restriction base="xs:string">
  <xs:enumeration value="NamedMap"/>
  <xs:enumeration value="NamedList"/>
  <xs:enumeration value="Map"/>
  <xs:enumeration value="List"/>
  <xs:enumeration value="Matrix"/>
  <xs:enumeration value="EnumOrString"/>
  <xs:enumeration value="EnumOrNumber"/>
  <xs:enumeration value="EnumBool"/>
  <xs:enumeration value="?NamedMap"/>
  <xs:enumeration value="?NamedList"/>
  <xs:enumeration value="?Map"/>
  <xs:enumeration value="?List"/>
  <xs:enumeration value="?Matrix"/>
  <xs:enumeration value="?EnumOrString"/>
  <xs:enumeration value="?EnumOrNumber"/>
</xs:restriction>
</xs:simpleType>

根元素显示了 XML 的整体设计。

 <xs:element name="root">
<xs:complexType>
<xs:choice maxOccurs="unbounded">
  <xs:element name="templated" type="Templated"/>
  <xs:element name="function" type="Function"/>
  <xs:element name="create">
<xs:complexType>
<xs:sequence>
  <xs:element name="measures" type="Measures" minOccurs="0"/>
  <xs:element name="name"/>
  <xs:group ref="ArgumentsGroup" maxOccurs="unbounded"/>
</xs:sequence>
  <xs:attribute name="id" type="PascalCaseString" use="required"/>
  <xs:attribute name="type" type="NameTypes" use="required"/>
</xs:complexType>
</xs:element>
</xs:choice>
</xs:complexType>
</xs:element>

相应的 NativeFunctions.XML 只需要有正确的头部,Visual Studio 才能识别 XSD。

 <?xml version="1.0" encoding="utf-8"?>
<root xmlns="http://www.tovica.com/schemas/cp3/nativefunctions/1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.tovica.com/schemas/cp3/nativefunctions/1 NativeFunctions.xsd">

<!-- Introductory examples -->
<function id="Function3" type="String">
<arg id="FirstArg" type="String"/>
<arg id="SecondArg" type="?String"/>
<arg id="ThirdArg" type="?String"/>
</function>

...

下面的示例演示了如何跳过参数或分配默认值。

 <function id="FunctionWithSkip" type="Date">
<arg id="AsOf" type="Date"/>
<arg id="Days" type="Integer">0</arg>
<arg id="Months" type="Integer">0</arg>
<arg id="Years" type="Integer">0</arg>
<skip id="BusDayConv"/>
<skip id="Calendars"/>
</function>

最后这些演示了为量化库添加类型安全的更高级功能,包括如何处理模型的度量值。

 <create id="CreateMap" type="Map">
  <name/>
  <arg id="Data" type="Any" isArray="2d"/>
  </create>

<create id="CreateFixings" type="Fixings">
  <name/>
  <arg id="AsOf" type="Date"/>
  <argT id="DatesAndValues" type="Matrix" T="QuantDatesAndNumbers"/>
</create>

<templated id="ModelOptionBlackScholes">
  <measures useLegacyMethod="yes"/>
  <arg id="AsOf" type="Date"/>
  <bool id="Payoff" type="QuantCallOrPut"/>
  <arg id="Spot" type="Double"/>
  <arg id="Strike" type="Double"/>
  <arg id="ExpiryDate" type="Date"/>
  <arg id="Rate" type="Curve"/>
  <arg id="Vol" type="Surface"/>
  <arg id="Fixings" type="?Fixings"/>
</templated>

<templated id="ModelOptionFD">
  <measures/>
  <arg id="AsOf" type="Date"/>
  <bool id="Payoff" type="QuantCallOrPut"/>
  <arg id="Spot" type="Double"/>
  <arg id="Strike" type="Double"/>
  <arg id="ExpiryDate" type="Date"/>
  <arg id="Rate" type="Curve"/>
  <process type="QuantVolOptions"/>
  <arg id="Fixings" type="?Fixings"/>
  <argT id="Options" type="?Map" T="QuantFDModelOptions"/>
</templated>

使用 T4 进行代码生成

除了 NativeFunctions.XML 和 XSD 文件,项目还包含一个 TT 文件。这是一种类似于 ASP.NET 的语言,Visual Studio 使用它来进行代码生成。编辑 TT 文件(或从其右键菜单中选择“运行自定义工具”)会导致 Visual Studio 编译并运行代码。文件的顶部相当标准:

<#@ template debug="true" hostspecific="true" language="C#" #>
<#@ output extension=".cs" #>
<#@ assembly name="System.Xml" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Xml.Linq" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Xml.Linq" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System" #>
// <#= new string('*', 78) #>
//GENERATED FILE!
//Do not edit this file manually; modify the associated NativeFunctions.xml file instead
// <#= new string('*', 78) #>

<#
Dictionary<string, Wrapper> wrappers;
var count = Initialise(out wrappers);
this.WriteLine("//Generated {0:N0} function wrappers", count);
#>
using ManagedAPI.Functions.Arguments;

...

输出文件将是 NativeFunctions.CS。Initialize() 调用负责将 NativeFunctions.XML 文件解析为合理的数据结构“wrappers”。然后对该数据结构进行重复循环以创建代码。

namespace ManagedAPI.Functions
{
  partial class NativeFunctions
  {
    /// <summary>
    /// Available quant functions - can be used directly to query existence.
    /// Alternatively, just use the Type property of the function argument wrappers.
    /// </summary>
    public enum Function
    {
      None = 0,
      <#
      this.PushIndent(new string(' ', 4 * 3));
      foreach (var p in wrappers)
      {
        this.WriteLine("{0},", p.Key);
      }
      this.PopIndent();
      #>
    }

有关所有详细信息,请参阅 NativeFunctions.TT 文件。

函数参数

此处也使用生成(Arguments.TT)来避免编写大量重复且繁琐的代码,这些代码需要支持所有参数类型,包括基本标量、向量和共享模式。通过重用这些结构体作为函数结果,也实现了一些代码的减少。例如,基本必需字符串参数的生成部分是:

 namespace Arguments
{
    [DebuggerDisplay("{Native.Type}: {Native.Boxed}")]
    public partial struct RequiredStringArg
    {
        readonly NativeArguments Buffer;
        public Variant Native { get { return Buffer[Index]; } }
        public readonly string Name;
        public readonly int Index;

        static void ThrowIfEmpty(Variant v, string name)
        {
            if (v.IsEmpty) throw new NativeMissingValueException(name);
        }
        internal void ThrowIfEmpty()
        {
            ThrowIfEmpty(Native, Name);
        }
        internal static String Get(string name, IntPtr r)
        {
            var v = new Variant(r);
            ThrowIfEmpty(v, name);
            return v.GetString(name);
        }
        internal void ThrowIfBadType()
        {
            ThrowIfEmpty();
            if (!Native.IsString) throw new NativeTypeMismatchException(Name);
        }

        internal RequiredStringArg(string name, NativeArguments buffer, int idx)
        {
            Name = name;
            Buffer = buffer;
            Index = idx;
        }
        public void Set(String value)
        {
            Native.SetString(value);
        }

        /// <summary>
        /// Given a shared variant (one allocated elsewhere and stored for reuse: e.g. TypedVariantCache)
        /// set the value of the given function argument field.
        /// This can be repeated on any number of arguments.
        /// The lifecycle of this argument now matches the shared variant, not the enclosing function.
        /// </summary>
        public void SetFromShared<SHARED>(SHARED sharedVariant)
            where SHARED : Interfaces.IVariantString
        {
            Buffer.UseVariantAgain(sharedVariant.Native, Index);
        }

        /// <summary>
        /// Given a shared variant (one allocated elsewhere and stored for reuse: e.g. TypedVariantCache), 
        /// transfer the given function argument into it for later reuse.
        /// This value can then be reused for any number of arguments on this function or any other.
        /// The lifecycle of this argument now matches the shared variant, not the enclosing function.
        /// </summary>
        public void Save(Variants.VariantString sharedVariant)
        {
            Buffer.StoreArgumentForReuse(sharedVariant.Native, Index);
        }

        /// <summary>
        /// Advanced: first use SaveResults, then pass that variant into this routine
        /// to reuse the result for one or more arguments.
        /// </summary>
        /// <param name="v">A subset of the variant that was used with SaveResults</param>
        public void SetFromResult<R>(R v) 
            where R : NativeFunctions.IResult
        {
            Buffer.UseVariantAgain(v.Native, Index);
        }
    }
}

只留下以下内容需要手动输入:

     partial struct RequiredStringArg
    {
        public void Set(StringBuilder value)
        {
            Native.SetString(value);
        }
    }

有关缓冲区处理的其他功能,包括作用域缓冲区和共享的本机参数或不同作用域的结果,可以在以下位置找到:

  • NativeBuffer - 参数和结果的原生 VARIANT 数组;
  • NativeArguments - 将 NativeBuffer 传递给函数并处理参数作用域;
  • TypedVariantCache - 用于重用本机的全局参数存储;
  • Builders and NamedBuilders - 用于高效设置类型化数组或矩阵(包括 Map 和 List)的辅助结构体;
  • QuantVolProcess, QuantFixings, QuantEnumOrNumber 等 - 用于设置参数和保存类型化结果及联合体的许多辅助结构体。

函数作为可 Dispose 结构体

函数本身的一般模式是从 NativeFunctions.XML 文件标记中完全生成,作为实现 IDisposable 的结构体——即,通常仅在“using”语句中使用。

生成的首部分是方便的常量和 NativeArguments 的必需字段,NativeArguments 本身通过 Thread Local Storage 与 NativeInstance 建立反向链接。

/// <summary>
/// Quant FunctionWithSkip function: must be created with any overload of New() and Disposed following the IDispose/using pattern
/// </summary>
/// <example>
/// using (var fn = FunctionWithSkip.New())
/// {
/// fn.AsOf.Set(DateTime.Now);
/// var res = fn.Invoke();
/// //Use res here - see Invoke() overloads for more details
/// }
/// </example>
public partial struct FunctionWithSkip : IDisposable
{
    public const NativeFunctions.Function Type = NativeFunctions.Function.FunctionWithSkip;
    public readonly static string Name = "FunctionWithSkip";
    public readonly static string Result = "FunctionWithSkip.Result";
    /// <summary>
    /// The buffer to use for this function call and it's results
    /// </summary>
    public NativeArguments Native;

每个参数都作为字段跟随。请注意,任何字段都可以被跳过或默认,而不仅仅是调用末尾的字段。

     //Number of used arguments: 4
    //SKIPPED: 2
    const int HighestArgCount = 4;

    /// <summary>
    /// Required: Date
    /// </summary>
    public RequiredDateArg AsOf;
    /// <summary>
    /// Required: Integer (DEFAULT = 0)
    /// </summary>
    public RequiredIntegerArg Days;
    /// <summary>
    /// Required: Integer (DEFAULT = 0)
    /// </summary>
    public RequiredIntegerArg Months;
    /// <summary>
    /// Required: Integer (DEFAULT = 0)
    /// </summary>
    public RequiredIntegerArg Years;
    //SKIP: public RequiredNoneArg BusDayConv;
    //SKIP: public RequiredNoneArg Calendars;

此方法依赖于“New()”来创建结构体,在其中显式地将参数槽号传递给每个参数,允许任何参数被跳过,甚至改变顺序,而不会改变任何调用代码。另请注意,可以为高级用法提供不同的缓冲区,例如每个模型一个缓冲区,或嵌套调用。

     /// <summary>
    /// Quant FunctionWithSkip function: must be Disposed - e.g. within a 'using' block
    /// </summary>
    public static FunctionWithSkip New(NativeArguments buffer = null)
    {
        if (buffer == null) buffer = NativeInstance.Current.Functions.DefaultBuffer;
        var res = new FunctionWithSkip
        {
            Native = buffer,
            AsOf = new RequiredDateArg("FunctionWithSkip.AsOf", buffer, 0),
            Days = new RequiredIntegerArg("FunctionWithSkip.Days", buffer, 1),
            Months = new RequiredIntegerArg("FunctionWithSkip.Months", buffer, 2),
            Years = new RequiredIntegerArg("FunctionWithSkip.Years", buffer, 3),
        };
        res.SetDefaults();
        return res;
    }

基本的参数类型检查和标量默认设置遵循,并通过部分函数进行扩展。

     [Conditional("DEBUG")]
    void VerifyArguments()
    {
        AsOf.ThrowIfBadType();
        Days.ThrowIfBadType();
        Months.ThrowIfBadType();
        Years.ThrowIfBadType();
        CustomVerifyArguments();
    }
    partial void CustomVerifyArguments();

    void SetDefaults()
    {
        Days.Set(0);
        Months.Set(0);
        Years.Set(0);
        CustomSetDefaults();
    }
    partial void CustomSetDefaults();

由于通过通用 Translator 使用 DLL 导出,因此量化库的原生二进制文件可以在不重新编译(或重新发布)使用它们的情况下进行更改。这意味着函数参数数量可能会发生变化,并且函数的可获得性也可能发生变化。以下内容允许进行编程检查,以便托管应用程序可以在功能不可用时干净地降级功能。

     /// <summary>
    /// Whether this function is available in the currently loaded Quant version
    /// </summary>
    public bool IsSupported { get { return NativeInstance.Current.Functions.IsSupported(Type); } }

    /// <summary>
    /// Throw if this function is not available in the currently loaded Quant version
    /// </summary>
    public void ThrowIfNotSupported()
    {
        NativeInstance.Current.Functions.ThrowIfNotSupported(Type);
    }

一旦提供了所有参数,就可以调用函数,传播任何异常,并在一次操作中对结果进行类型检查。请注意,参数结构体是如何重用于执行后一项检查的。

     /// <summary>
    /// Result is of type: Date
    /// </summary>
    public Date Invoke()
    {
        VerifyArguments();
        var raw = Native.Call(Type);
        return RequiredDateArg.Get(Result, raw);
    }

或者,可以通过将调用分开来避免任何异常。在第二次调用之间或之后,NativeArguments 上有方法可直接访问原始结果、原始错误代码或其解码值。

     /// <summary>
    /// Advanced: use to avoid the Quant exception on an error - if return true, 
    /// call InvokeEnd() to obtain the result. See Native for accessing the error code or message.
    /// Result is of type: Date
    /// </summary>
    public bool InvokeBegin()
    {
        VerifyArguments();
        var code = Native.CallRaw(Type);
        return code == 0;
    }

    /// <summary>
    /// Advanced: use only if InvokeBegin() return true to access the result in a typesafe manner.
    /// See Native for accessing the error code or message.
    /// Result is of type: Date
    /// </summary>
    public Date InvokeEnd()
    {
        var raw = Native.RawResult;
        return RequiredDateArg.Get(Result, raw);
    }

原生结果的生存期可以转移到不同的作用域——即,这可以防止“using”块在释放参数的同时释放结果。

     /// <summary>
    /// Advanced: given shared variant (allocated elsewhere e.g. via a TypedVariantCache), 
    /// store the result of this function in it.
    /// This result can then be reused on any number of arguments.
    /// The lifecycle of the result is now external to this enclosing function.
    /// </summary>
    public void SaveResults(ref VariantResult shared)
    {
        Native.StoreResultForReuse(ref shared, Result);
    }

最后,生成函数末尾的代码,包括释放参数和结果的原生值(但不是 NativeBuffer 本身)。

     /// <summary>
    /// Advanced: Use this before using this structure again, either in a loop or when stored as a variable
    /// </summary>
    public void ResetToDefaults()
    {
        Free();
        SetDefaults();
    }

    internal void Free()
    {
        Native.FreeForReuse(HighestArgCount);
    }

    void IDisposable.Dispose()
    {
        Free();
    }
}

当访问结果成本较高时,需要提供额外的选项。例如,为字符串向量提供了以下选项:

 /// <summary>
/// Result is of type: Vector of Strings
/// USAGE: 
/// 1. This overload uses a default Adaptor to produce appropriately typed arrays
/// 2. Alternatives: See other overloads
/// 3. Advanced: See InvokeAsVariant()
/// </summary>
public String[] Invoke()
{
    var a = new Adaptors.ArrayOutput<String>();
    Invoke(ref a);
    return a;
}

/// <summary>
/// Result is of type: Vector of Strings
/// USAGE: 
/// 1. Use this overload and an appropriate implementation of the IOutput callback
///    to fill in a result (see Adaptors namespace);
/// 2. Alternatives: See other overloads
/// 3. Advanced: See InvokeAsVariant()
/// The return result is the number of elements in the vector.
/// </summary>
public int Invoke<T>(ref T output) where T : Variant.IOutput<String>
{
    VerifyArguments();
    var raw = Native.Call(Type);
    return RequiredStringVectorArg.Get(Result, raw, ref output);
}

/// <summary>
/// Result is of type: Vector of Strings - the result will be verified as a vector 
/// but the element type is NOT verified.
/// Advanced: Use this overload to access the raw results.
/// </summary>
public Results InvokeAsVariant()
{
    VerifyArguments();
    var raw = Native.Call(Type);
    return RequiredStringVectorArg.Get(Result, raw);
}

最后,接受度量列表的函数也提供额外的支持,例如设置度量本身的多种方法。这以便利性为代价来牺牲效率。

/// <summary>
/// Quant ModelOptionFD function: must be Disposed - e.g. within a 'using' block
/// NOTE: This is a convenience overload
/// For more advanced scenarios set the measures directly on the function itself
/// </summary>
public static ModelOptionFD New(ICollection<QuantMeasure> measures)
{
    var buffer = NativeInstance.Current.Functions.DefaultBuffer;
    buffer.Functions.SetMeasures(buffer[0], measures, measures.Count);
    return New(buffer);
}

/// <summary>
/// Advanced: Sets the measures 
/// The usual easier approach is to use the New() overloads when creating the function itself
/// </summary>
public void SetMeasures(IEnumerable<QuantMeasure> measures, int count)
{
    Native.Functions.SetMeasures(Measures.Native, measures, count);
}

类型化矩阵

依赖于非类型化数组尤其令人痛苦,特别是当所需形状和内容取决于某个其他参数时。例如,FunctionComplex() 有一个参数标记为:

 <argT id="LowBarrier" type="?Matrix" T="QuantContinuousBarrierSchedule,
                                        QuantDiscreteBarrierSchedule"/>

这会生成以下两种类型的联合体:

public OptionalMatrixArg<QuantContinuousBarrierSchedule,QuantDiscreteBarrierSchedule> LowBarrier;

从前面摘录,下面的内容演示了如何使用第一个构建器设置 QuantContinuousBarrierSchedule:

     var bldBarrier = fn.LowBarrier.SetWithBuilder1(2);
    //Optimal approach when translating an array of structures into individual arrays
    bldBarrier.StartDates.Set(schedule, t => t.Key);

该参数是手动编码为通用的,接受联合体的类型列表,在本例中是两个。

 public struct OptionalMatrixArg<T, U>
where T : struct, IMatrixBuilder<T>
where U : struct, IMatrixBuilder<U>
{
    readonly NativeArguments Buffer;
    public Variant Native { get { return Buffer[Index]; } }
    public readonly string Name;
    public readonly int Index;
    
    ....

然后可以通过委托给适当的通用类型实现的接口来公开每个构建器。

     /// <summary>
    /// Set the native variant using a typed builder.
    /// </summary>
    public T SetWithBuilder1(int rows, int cols = -1)
    {
        var m = new T();
        return m.New(Native, rows, cols);
    }

公开像 StartDates 这样的内容涉及创建一系列类参数结构体,这些结构体按列操作,而不是操作预先分配的混合矩阵,如下所示:

public partial struct RequiredDateColumn
{
    readonly Variant.Vector Native;

    public void Set(IList<Date> value)
    {
        Native.SetDateColumn(value);
    }
    public void SetAt(int idx, Date value)
    {
        Native[idx].SetDate(value);
    }
    
    ....
}

最后,可以使用上述方法手动编码 QuantContinuousBarrierSchedule。请注意,在本例中,量化库接受零返利,但不接受空支付日期,这使得事情更加复杂。

 public struct QuantContinuousBarrierSchedule : IMatrixBuilder<QuantContinuousBarrierSchedule>
{
    public static readonly string Name = "ContinuousSchedule";
    public const int MaxColumns = 5;

    public readonly RequiredDateColumn StartDates, EndDates;
    public readonly RequiredDoubleColumn Levels;
    public readonly OptionalDoubleColumn Rebates;
    public readonly OptionalDateColumn PayDates;

    public bool HasPayDates { get { return Columns == MaxColumns; } }
    public readonly int Columns;

    internal QuantContinuousBarrierSchedule(Variant v, int rows, int cols = -1)
        : this()
    {
        Columns = cols <= 4 ? 4 : MaxColumns;
        var vector = v.SetNewMatrix(Columns, rows);
        StartDates = new RequiredDateColumn(vector); vector++;
        EndDates = new RequiredDateColumn(vector); vector++;
        Levels = new RequiredDoubleColumn(vector); vector++;
        Rebates = new OptionalDoubleColumn(vector); vector++;
        if (Columns == MaxColumns) PayDates = new OptionalDateColumn(vector); vector++;
    }

QuantDiscreteBarrierSchedule 具有不同数量的列和类型。有关更多计划,请参见 QuantAsianAveragingSchedule 和 QuantFaderBarrierSchedule。

枚举映射

带有属性的枚举是一种声明式支持,用于将“魔术”字符串替换为更清晰的枚举。例如,如果用户为参数指定了 Continuous,以下代码将确保将“C”传递给量化库。

 public enum QuantBarrierType
{
    [EnumName("C")]
    Continuous,
    [EnumName("D")]
    Discrete,
}

同样,用清晰的两值枚举替换烦人的布尔值可以创建一个更好的 API。例如,如果用户为参数指定 Put,以下代码将传递 FALSE 给量化库,如果指定 Call,则传递 TRUE。

 public enum QuantCallOrPut
{
    Put = 0,
    Call,
}

量化库可能支持许多替代方案,并且可能需要将这些值规范化为单个值,以便在编写符合 XSD 的 XML 时,例如。在以下示例中,所有替代方案都映射到主名称(第一个名称)。

 /// <summary>
/// The available frequencies with primary and alternative names
/// </summary>
public enum QuantFrequency
{
    [EnumName("Daily", "1D")]
    Daily,
    [EnumName("Weekly", "1W")]
    Weekly,
    [EnumName("TwoWeekly", "BiWeekly", "2W")]
    TwoWeekly,
    [EnumName("FourWeekly", "28D", "4W")]
    FourWeekly,
    [EnumName("Monthly", "1M")]
    Monthly,
    [EnumName("Quarterly", "3M")]
    Quarterly,
    [EnumName("SemiAnnual", "Semi", "6M")]
    SemiAnnual,
    [EnumName("Annual", "1Y", "12M")]
    Annual,
    Term,
}

最后,量化库开发人员选择的一些名称可能过于晦涩或模棱两可。

 public enum QuantMeasure
{
    AccrualEndDates = 0,
    AccrualStartDates,
    [EnumName("FullDiscountRho")]
    BucketedDiscountRho,
    Cashflows,
    Coupons,
    Dates,
    [EnumName("DCFs")]
    DayCountFractions,
    Delta,
    [EnumName("DFs")]
    DiscountFactors,
    ....

此外,枚举转换为字符串的众所周知的问题也在此处出现——即,对枚举调用 ToString() 不仅速度慢,而且会产生重复(非 interned)的字符串,并且允许指定数字。

以下显示了一个类实现的第一个部分,该类相当高效地解决了这些问题。

 [DebuggerStepThrough]
public static class Enum<T> where T : struct, IConvertible
{
    #region Accessors
    /// <summary>
    /// Short name of the enumeration
    /// </summary>
    public readonly static string Name;

    /// <summary>
    /// String names in sorted ascending order
    /// These strings are all interned
    /// See GetValueByIndex for way to get value given index in this array
    /// </summary>
    public readonly static string[] SortedNames;

    /// <summary>
    /// Values in numerical sorted ascending order
    /// </summary>
    public readonly static T[] SortedValues;

    /// <summary>
    /// Number of items
    /// </summary>
    public static int Count { get { return SortedNames.Length; } }

    /// <summary>
    /// Alternative string mappings
    /// NOTE: May be null if none present
    /// </summary>
    public readonly static SortedList<string, string> Alternatives;

    /// <summary>
    /// Given the index of a name in the SortedNames collection, return the corresponding value
    /// See SortedNames and SortedValues.
    /// </summary>
    public static T GetValueByIndex(int idx)
    {
        return SortedValues[_name2value[idx]];
    }

    /// <summary>
    /// Given the index of a value in the SortedValues collection, return the corresponding name
    /// See SortedNames and SortedValues.
    /// </summary>
    public static string GetNameByIndex(int idx)
    {
        return SortedNames[_value2name[idx]];
    }

    #endregion

有许多可用方法,包括扩展方法——有关详细信息,请参阅 EnumConverter.cs。初始化通过静态构造函数完成,如下所示。

     #region One time initialisation
    readonly static int[] _value2name, _name2value;
    static Enum()
    {
        var type = typeof(T);
        if (!type.IsEnum) throw new InvalidOperationException("Type is not an enum: " + type.FullName);
        Name = type.Name;

        //Store stored names with corresponding values
        //SortedNames = Enum.GetNames(type);
        SortedValues = (T[])Enum.GetValues(type);

        //Allow for name to be overridden
        var fields = typeof(T).GetFields(BindingFlags.Public | BindingFlags.Static);
        //XmlNameAttribute
        SortedNames = new string[fields.Length];
        for (int i = 0; i < fields.Length; i++)
        {
            var field = fields[i];
            var name = field.Name;
            var attributes = (EnumNameAttribute[])field.GetCustomAttributes(typeof(EnumNameAttribute), false);
            if (attributes.Length == 1)
            {
                name = attributes[0].Name;
                var alternatives = attributes[0].Alternatives;
                if (alternatives != null)
                {
                    Alternatives = Alternatives ?? new SortedList<string, string>(fields.Length, StringComparer.Ordinal);
                    foreach (var a in alternatives) Alternatives.Add(a, name);
                }
            }
            SortedNames[i] = name;
        }

        //And sort
        Array.Sort(SortedNames, SortedValues, StringComparer.Ordinal);

        //Now sort by value for reverse lookup
        _value2name = new int[SortedNames.Length];
        for (int i = 0; i < _value2name.Length; i++) _value2name[i] = i;
        Array.Sort(SortedValues, _value2name, Comparer);

        //And store name to value lookup
        _name2value = new int[SortedNames.Length];
        for (int i = 0; i < _name2value.Length; i++) _name2value[_value2name[i]] = i;
    }
    #endregion

原生主题

讨论高级 C# 互操作主题和 Windows C++ 实现。

Translator

C# 代码需要一个 P-Invoke 友好的 API 来调用,这就是 Translator.DLL 提供的,首先在运行时遍历量化库 (NativeAPI.DLL) 的导出地址表,报告所有导出的函数。

//_GetExportedFunctions@12
extern "C" HRESULT __declspec(dllexport) __stdcall
GetExportedFunctions(HMODULE module, LPSAFEARRAY* pDemangled, LPSAFEARRAY* pAddresses)
{
    DWORD cNames = 0;
    *pDemangled = nullptr;
    *pAddresses = nullptr;

    auto pHeaders = ImageNtHeader(module);
    if (pHeaders)
    {
        auto pTable = (PIMAGE_EXPORT_DIRECTORY)::ImageDirectoryEntryToData(module, TRUE, 
                                                 IMAGE_DIRECTORY_ENTRY_EXPORT, &cNames);
        cNames = 0;
        if (pTable)
        {
            cNames = pTable->NumberOfNames;
            auto pNames = (DWORD*)(pTable->AddressOfNames + (DWORD)module);
            auto pFunctions = (DWORD*)(pTable->AddressOfFunctions + (DWORD)module);
            if (cNames > 0 && pNames && pFunctions)
            {
                //Assume the full size - will use empties to indicate failures
                *pDemangled = ::SafeArrayCreateVector(VT_BSTR, 0, cNames);
                *pAddresses = ::SafeArrayCreateVector(VT_I4, 0, cNames);
                
                auto pCurrD = (BSTR*)(*pDemangled)->pvData;
                auto pCurrA = (LPVOID*)(*pAddresses)->pvData;

                auto buffer = new char[MaxUndecoratedNameLength + 1];
                auto i = cNames;
                do
                {
                    auto pName = (LPCH)(*pNames + (DWORD)module);
                    auto pFunction = (PROC)(*pFunctions + (DWORD)module);
                    if (pName && pName[0] && pFunction)
                    {
                        auto cbSymbol = ::UnDecorateSymbolName(pName, buffer, 
                                          MaxUndecoratedNameLength + 1, UNDNAME_COMPLETE);
                        if (cbSymbol > 0 && cbSymbol < MaxUndecoratedNameLength
                                                           - 4 /* Misbehaving function */)
                        {
                            *pCurrA = pFunction;
                            *pCurrD = (BSTR)::SysAllocStringLen(nullptr, cbSymbol);
                            ::MultiByteToWideChar(CP_UTF7, 0, buffer, cbSymbol + 1, *pCurrD, cbSymbol);
                        }
                    }
                    ++pCurrA;
                    ++pCurrD;
                    ++pFunctions;
                    ++pNames;
                    --i;
                } while (i > 0);
                delete buffer;
            }
        }
    }
    return cNames > 0 ? S_OK : E_FAIL;
}

NativeFactory 中相应的 P-Invoke 签名是:

delegate int _GetExportedFunctions(IntPtr module,
    [MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_BSTR)] 
    out string[] pDemangled,

    [MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_I4)]
    out IntPtr[] pAddresses
);

Translator 本身必须具有线程亲和性,以便在多个量化库副本之间轻松共享同一个实例,同时仍然提供一个存储原生 C++ 异常消息的位置,每个副本一个。

static __declspec(thread) char TLS_Error[255];

//_GetThreadLastError@0
extern "C" LPSTR __declspec(dllexport) __stdcall
GetThreadLastError(void)
{
    return TLS_Error;
}

宏和可变参数模板被用来更轻松地创建相同的包装器函数,这些函数仅在预期的参数数量上有所不同。

//1. Arguments - note that args[0] is for the result
#define F_ARGS(NUMBER, ...) &args[NUMBER]
//2. Invoker
#define DECLARE_FUNCTION(COUNT) \
template<typename... Params> \
static HRESULT __stdcall \
MACRO_CAT(Invoke, COUNT)(LPVOID fn, VARIANT* args) \
{ \
  try \
  { \
    args[0] = ((VARIANT(__stdcall *)(Params*...))fn)(MACRO_ARGS(COUNT,F_ARGS)); \
    return S_OK; \
  } \
  catch (std::exception& ex) \
  { \
    ::strncpy_s(TLS_Error, ex.what(), _TRUNCATE); \
    return E_FAIL; \
  } \
}

以 COUNT 为 3 的情况为例,它会扩展成大致相当于这样的内容:

HRESULT Invoke3(FUNCTION2 fn, VARIANT* args)
{
  try
  {
    args[0] = FUNCTION2(&args[1], &args[2]);
    return S_OK;
  }
  catch (std::exception& ex)
  {
    TLS_Error = ex.what();
    return E_FAIL;
  }
}

也就是说,给定一个接受两个参数的量化库函数的地址,用这些参数调用它,存储结果并记录 TLS 缓冲区中的任何异常消息。

关键在于,这意味着相同的 P-Invoke 签名可以用于所有量化库函数,因此量化库版本之间参数数量的变化无关紧要。这使得量化库可以更改而无需重新编译 Translator,甚至不需要重新编译使用它的 C# 应用程序。

delegate int _FunctionWrapper(IntPtr function, IntPtr buffer);

//Max arguments - must be at most equal to the equivalent in Translator.dll
internal const int MaxFunctionArguments = 12;
readonly static _FunctionWrapper[] _FunctionWrappers = 
                         new _FunctionWrapper[MaxFunctionArguments];

如上述代码所示,支持的参数数量必须在编译时固定。为简单起见,这也在 C# 中完成,而不是仅仅从 Translator 导出另一个函数。然后使用宏来“冲压”代码,如下所示:

//The wrappers: append more as needed
DECLARE_FUNCTION(0)
DECLARE_FUNCTION(1)
...
DECLARE_FUNCTION(12)

//Table of wrappers: append more as needed
static const PROC Invokers[] = 
{
    ADDRESS_FUNCTION(0),
    ADDRESS_FUNCTION(1),
    ...
    ADDRESS_FUNCTION(12),
};

//_GetFunctionWrapper@4
extern "C" LPVOID __declspec(dllexport) __stdcall
GetFunctionWrapper(int argc)
{
    //Args is number of arguments excluding the return result
    if (argc >= 0 && argc < _countof(Invokers)) return Invokers[argc];
    return nullptr;
}

最后,Managed.DLL 中的 NativeFactory 负责定位 Translator.DLL,加载它,并按如下方式修复所有入口点:

public static bool SearchForNativeBinariesAndSetupTranslator()
{
  if (!SearchForNativeBinaries()) return false;

  //Expected to be in assembly location
  var check = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), TRANSLATOR);
  if (!File.Exists(check)) return false;

  var hTranslator = NativeHelpers.NativeLoadLibrary(TRANSLATOR);
  _ExportedFunctions = NativeHelpers.GetDelegate<_GetExportedFunctions>(hTranslator, "_GetExportedFunctions@12");
  _GetLastError = NativeHelpers.GetDelegate<GetLastErrorFunctionType>(hTranslator, "_GetThreadLastError@0");

  //Hook up the faster helpers
  Variant.NativeFree = NativeHelpers.GetDelegate<Variant.VariantHelperType>(hTranslator, "_VariantsFree@8");
  Variant.NativeInit = NativeHelpers.GetDelegate<Variant.VariantHelperType>(hTranslator, "_VariantsInit@8");

  //Get the wrapper accessor
  var getWrapper = NativeHelpers.GetDelegate<_GetFunctionWrapper>(hTranslator, "_GetFunctionWrapper@4");
  //And create the wrappers
  for (int i = 0; i < MaxFunctionArguments; i++)
  {
    var wrapper = getWrapper(i);
    Debug.Assert(wrapper != null, "MaxArgument count needs to be increased in Translator");
    _FunctionWrappers[i] = NativeHelpers.GetDelegate<_FunctionWrapper>(hTranslator, wrapper);
  }
  return true;
}

Translator 暴露了函数包装器的地址以及 NativeAPI 导出的所有函数的完整名称修饰。出于性能原因,最好将地址直接“烘焙”到 IL 中——即,给定 NativeAPI 中的一个函数及其在 Translator 中的相应包装器,创建一个单一的 C# 委托,如下所示:

internal delegate int NativeFunctionType(IntPtr buffer);

/// <summary>
/// Create a delegate around a compiled expression that calls the given NativeAPI
/// function with the given 'argc' argument count via the Translator
/// </summary>
/// <param name="address">Native address of function within NativeAPI</param>
/// <param name="argc">Number of VARIANT* arguments; function must return a VARIANT</param>
/// <returns></returns>
internal static NativeFunctionType CreateFunction(IntPtr address, int argc)
{
  if (_SxSPathsByVersion == null) throw new InvalidOperationException("Call SearchForNativeBinariesAndSetupTranslator first");
  if (argc >= _FunctionWrappers.Length) throw new IndexOutOfRangeException("GetFunction: max function arguments exceeded");

  var wrapper = _FunctionWrappers[argc];
  var arg1 = Expression.Parameter(typeof(IntPtr), "buffer");

  //var res = wrapper(addresses, buffer)
  var exp = Expression.Invoke(Expression.Constant(wrapper), Expression.Constant(address), arg1);
  var lambda = Expression.Lambda<NativeFunctionType>(exp, arg1);
  return lambda.Compile();
}

并发和清单

Translator.DLL 以传统方式加载:通过 Windows LoadLibrary 函数,如下所示:

internal static IntPtr NativeLoadLibrary(string fullFileName)
{
    var res = LoadLibraryW(fullFileName);
    if (res != IntPtr.Zero) return res;
    var err = Marshal.GetLastWin32Error();
    throw new FileLoadException(string.Format("Unable to load library (0x{0:X8}) '{1}'", err, fullFileName));
}

然而,为了加载同一组相互依赖的 DLL 的多个实例,需要以下操作:

  • 每个物理组必须有一个唯一的路径——例如,位于不同的子文件夹中,如“Build\Debug\1\*”和“Build\Debug\2\*”;
  • 所有 DLL 都必须移除嵌入的清单——例如,如果与 CRT 等动态链接,则会存在此清单;
  • 必须创建一个单一的清单,列出物理组中的所有 DLL(以及 CRT 等,如果需要);
  • 使用 LoadLibrary,但在激活上下文内——即,在该清单处于活动状态时。

因此,在每个子文件夹“1”和“2”中,都有 NativeAPI 及其依赖的 Utils 的副本——为简单起见,两者都与 CRT 静态链接,因此它们没有嵌入清单。现在,可以在两个位置创建类似“NativeAPI.Manifest”的 XML 文件,其内容相同。

<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
<assembly xmlns='urn:schemas-microsoft-com:asm.v1' manifestVersion='1.0'>
  <assemblyIdentity type='win32' name='NativeAPI' version='1.0.0.0' processorArchitecture='x86'/>
  <description>Native API</description>
  
  <file name='NativeAPI.DLL'/>
  <file name='Utils.DLL'/>
</assembly>

然后使用清单来创建激活上下文,仅在 LoadLibrary 调用期间有效。这会在不同的线程上并发发生,每个线程一个物理组或实例,C# 的 TLS 版本用于将 NativeInstance 类“分配”给当前线程。

public class NativeInstance
{
    public readonly int Id;
    public readonly Version Version;
    public readonly string EntryPoint;
    public readonly string ManifestName;

    public readonly NativeFunctions Functions;

    /// <summary>
    /// TLS: Provides access to the current native instance assigned to this thread, or null
    /// </summary>
    public static NativeInstance Current { [DebuggerStepThrough] get { return _currentInstance; } }

    #region Native loading
    [ThreadStatic]
    static NativeInstance _currentInstance;

    internal NativeInstance(int instanceId, Version version, string targetDLL, string manifest = null)
    {
        if (_currentInstance != null) 
            throw new InvalidOperationException("An NativeInstance is already assigned to this thread: "
             + _currentInstance.Id);

        Id = instanceId;
        Version = version;
        EntryPoint = targetDLL;

        uint? cookie = null;
        if (manifest != null)
        {
            ManifestName = Path.GetFileName(manifest);
            cookie = NativeHelpers.NativePushActivationContext(Path.GetDirectoryName(manifest), manifest);
        }
        try
        {
            //Load specific instance DLLs
            var hModule = NativeHelpers.NativeLoadLibrary(targetDLL);
            //Resolve all exported functions
            Functions = new NativeFunctions(this, hModule, version);
        }
        finally
        {
            if (cookie != null) NativeHelpers.NativePopActivationContext(cookie.Value);
        }

        //If we get here all is well
        _currentInstance = this;

        //Safe to map all measures
        Functions.MapAllUsedMeasures();
    }
    #endregion

解码 Variant

NativeAPI 完全基于 VARIANT,鉴于 .NET 最初的“Com+ v2”版本,这并不奇怪,可以几乎完美地映射到 Object,但是:

  • 常用的 VT_ERROR 类型与整数(如 VT_I4)无法区分;
  • .NET v2.0 之前的所有相关 Marshal API 都是这样设计的,因此所有内容都必须双向装箱;
  • 数组映射需要每个元素都进行装箱。

后者问题对于量化库尤其糟糕,因为金融工具(例如期权)的定价通常涉及同时要求相当多的风险和信息“度量”——即,结果是不同形状的数组的数组。

从积极的方面来看,可能的输入和输出类型以及数组形状通常相当小。例如,传递一个向量(一维数组)会被转换为二维数组——所以只需要处理后者类型。

使用手动解码,包括不安全的 C# 代码,以避免 .NET 内置支持的问题,所有这些都基于单个原生内存块(16 字节)的地址,该内存块持有 VARIANT 如下:

public struct Variant
{
    readonly IntPtr _variant;

    #region Helpers

    #region Supported variant types
    readonly static VarEnum[] _supported = new[] 
    {
        VarEnum.VT_EMPTY,
        VarEnum.VT_I4,
        VarEnum.VT_R8,
        VarEnum.VT_DATE,
        VarEnum.VT_BSTR,
        VarEnum.VT_BOOL,
        VarEnum.VT_VARIANT|VarEnum.VT_ARRAY,

    };

    static bool VarTypeIsSupported(VarEnum type)
    {
        return Array.BinarySearch(_supported, type, Enum<VarEnum>.Comparer) >= 0;
    }

    static Variant()
    {
        Array.Sort(_supported);
    }
    #endregion

由于 Translator 暴露的 API 涉及一个连续的 VARIANT 块,因此解码器可以连接到其中任何一个,如下所示:

     internal Variant(IntPtr ptr, int index)
    {
        _variant = ptr + index * 16;
    }

VARIANT 是一个经典的 C 联合体,带有一个 2 字节的鉴别符,可以安全地清零(VT_EMPTY),前提是它不包含 BSTR 或 SAFEARRAY。

     #region Destruction
    //Free variant, resetting to VT_EMPTY
    [SuppressUnmanagedCodeSecurity]
    internal void Free()
    {
        if (IsReferenceType) NativeHelpers.NativeNativeFree(_variant);
        else Reset();
    }

    //Force type to VT_EMPTY without freeing first
    [SuppressUnmanagedCodeSecurity]
    internal void Reset()
    {
        Marshal.WriteInt16(_variant, (short)VarEnum.VT_EMPTY);
    }
    #endregion

为了便于调试,尤其是在量化库使用 VARIANT 类型不一致的情况下,将原始类型(不带任何标志,如 VT_ARRAY)和 .NET 对原始数据的解释都暴露出来很有用。

     public VarEnum Type { get { return (VarEnum)((byte)VarType); } }
    internal VarEnum VarType { get { return (VarEnum)Marshal.ReadInt16(_variant); } }

    public object Boxed { get { return !IsEmpty ? Marshal.GetObjectForNativeVariant(_variant) : null; } }

使用嵌套类型来处理向量——即,它表示 SAFEARRAY 的数据负载,其元素类型为 VARIANT。

    //Wrapper for native contiguous array of VARIANTs
    public struct Vector
    {
        readonly IntPtr _variant;
        readonly int _count;

        public static readonly Vector None;

        #region Construction

        //Attach to given variant
        internal Vector(IntPtr ptr, int count)
        {
            _variant = ptr;
            _count = count;
        }

        //Attach to variant at index in given variant array
        private Vector(IntPtr ptr, int index, int count)
        {
            _variant = ptr + index * 16;
            _count = count;
        }
        #endregion

同样,为了便于 .NET 调试,会暴露原始类型和 .NET 对原始数据的解释。

         public object Boxed { get { return Marshal.GetObjectsForNativeVariants(_variant, _count); } }

出于样式原因,所有不安全的(静态)C# 使用都进行了范围限定。直接解码依赖于查看 VARIANT 的原生内存布局。

     [SuppressUnmanagedCodeSecurity]
    internal unsafe class Unsafe
    {
        static VarEnum GetType(byte* pVariant)
        {
            return (VarEnum)(*(ushort*)pVariant);
        }

        //Prototype for the following unchecked accessors
        delegate T VariantGet<T>(byte* pVariant);

        static DateTime UncheckedDate(byte* pVariant)
        {
            return DateTime.FromOADate(*(double*)(pVariant + 8));
        }
        static double UncheckedDouble(byte* pVariant)
        {
            return *(double*)(pVariant + 8);
        }
        static int UncheckedInteger(byte* pVariant)
        {
            return *(int*)(pVariant + 8);
        }
        static bool UncheckedBoolean(byte* pVariant)
        {
            return *(int*)(pVariant + 8) != 0;
        }
        static string UncheckedString(byte* pVariant)
        {
            var ptr = *(int*)(pVariant + 8);
            return ptr != 0 ? Marshal.PtrToStringBSTR(new IntPtr(ptr)) : null;
        }

请注意,布尔值映射被定义为“非 0”,以避免 VBA 仅与“-1”严格比较等微妙问题。同样,量化库预计不会使用零长度的 BSTR,这在反向情况中会得到仔细检查。

以下例程用于直接手动解码 BSTR 到 StringBuilder,以在可能的情况下避免使用不可变的 .NET 字符串。它依赖于原生内存布局在 UTF16 字符串数据本身开始之前就具有长度。

             internal static int GetBSTR(IntPtr pBStr, StringBuilder sb)
            {
                if (pBStr != IntPtr.Zero)
                {
                    var len = Marshal.ReadInt32(pBStr - 4) / 2;
                    if (len > 0)
                    {
                        sb.EnsureCapacity(len + sb.Length);
                        for (int i = 0; i < len; i++)
                        {
                            sb.Append((char)Marshal.ReadInt16(pBStr, i * 2));
                        }
                    }
                }
                return sb.Length;
            }

然后,一般方法是为每种类型调用一系列例程,如下面的“double”情况所示,它使用 NaN 来表示 VT_EMPTY 情况。

         static T GenericGetElement<T>(string field, VarEnum check, byte* pv, 
                                      VariantGet<T> uncheckedFunction, T @default = default(T))
        {
            VarEnum type;
            if ((type = GetType(pv)) == check)
            {
                return uncheckedFunction(pv);
            }
            if (type == VarEnum.VT_EMPTY || type == VarEnum.VT_ERROR) return @default;
            throw ErrorCOM(field, "Type mismatch - expected {0} but got {1}", check.ToString(), type.ToString());
        }

        internal static double GetDouble(string field, IntPtr v)
        {
            return GenericGetElement<double>(field, VarEnum.VT_R8, (byte*)v.ToPointer(), UncheckedDouble, double.NaN);
        }

    internal double GetDouble(string field)
    {
        return Unsafe.GetDouble(field, _variant);
    }

对于数组解码,采用了一种常见的反装箱模式:将一个泛型约束到一个接口直接用作参数。此外,“ref”用于确保只传递实现该接口的结构体的地址,也用于更新任何状态。检查此方法的 IL 字节码表明使用了“constrained calls”,并且没有发生装箱。

     public interface IOutput<T>
    {
        void OnMatrix(int row, int col, T data);
        void OnColumn(int row, T data);
    }

SAFEARRAY 的实际解码涉及一些繁琐的不安全 C# 代码,因此未在此处显示,但围绕它的例程展示了反装箱模式。

         static void UncheckedGetColumn<T, O>(string field, VarEnum check, int rows, ref byte* pvData, 
                  VariantGet<T> f, ref O output, T @default = default(T))
            where O : IOutput<T>
            where T : IEquatable<T>
        {
            if (pvData == null || rows <= 0) return;
            output.OnColumn(~rows, @default);
            for (int i = 0; i < rows; i++)
            {
                var raw = GenericGetElement(field, check, pvData, f, @default);
                if (!EqualityComparer<T>.Default.Equals(raw, @default)) output.OnColumn(i, raw);
                pvData += 16;
            }
        }
        static int UncheckedGetVector<T, O>(string field, VarEnum check, byte* pVariant, 
                                                  VariantGet<T> f, ref O output, T @default = default(T))
            where O : IOutput<T>
            where T : IEquatable<T>
        {
            int cDim, len, dummy;
            var pvData = UncheckedArray(field, pVariant, out cDim, out len, out dummy);
            if (pvData == null) return 0;
            if (cDim == 2) throw ErrorCOM(field, "Internal vector problem - vector expected but got matrix");
            UncheckedGetColumn(field, check, len, ref pvData, f, ref output, @default);
            return len;
        }

        static int GenericGetVector<T, O>(string field, VarEnum check, byte* pv, 
                                     VariantGet<T> uncheckedFunction, ref O output, T @default = default(T))
            where O : IOutput<T>
            where T : IEquatable<T>
        {
            VarEnum type;
            if ((type = GetType(pv)) == (VarEnum.VT_ARRAY | VarEnum.VT_VARIANT))
            {
                return UncheckedGetVector(field, check, pv, uncheckedFunction, ref output, @default);
            }
            if (type == VarEnum.VT_EMPTY || type == VarEnum.VT_ERROR) return 0;
            throw ErrorCOM(field, "Type mismatch - expected {0} but got {1}", check.ToString(), type.ToString());
        }

提供了许多例程,涵盖了所有类型,包括对相同类型的矩阵以及更常见的混合类型的特定支持——即,其中每一列都是不同的类型。

相应的设置例程稍微棘手一些,因为它们必须规范化输入。例如,以下显示了如何利用 P-Invoke 对 StringBuilder 的特殊处理来避免在创建 BSTR 时出现重复。

         static void UncheckedNewString(byte* pVariant, StringBuilder value)
        {
            if (value.Length > 0)
            {
                *(int*)(pVariant + 8) = NativeHelpers.NativeAllocBSTR(value).ToInt32();
                return;
            }
            //NativeAPI disallows zero-length strings
            *(int*)(pVariant + 8) = 0;
        }
        
        [DllImport(OLEAUT32_DLL, ...)]
        //The first argument will be marshalled by pinning the managed buffer
        private static extern IntPtr SysAllocStringLen([In] StringBuilder sb, int cch);

该反装箱模式与复杂的基于委托的签名一起使用,以实现将数据从结构体集合流式传输到单个类型列的关键功能,而无需进行任何装箱。

 static class Singleton<T>
where T : IEquatable<T>
{
  public static readonly Func<T, T> Identity = x => x;
}

static byte* UncheckedSetColumn<TCollection, TElement, TEnumerable>(VarEnum type, int skip, int take, byte* pvData,
VariantSet<TElement> setter, TEnumerable input, Func<TCollection, TElement> selector, TElement @default)
where TElement : IEquatable<TElement>
where TEnumerable : IEnumerable<TCollection>
...

运行示例

有三个互斥的代码分支——即,其中两个被注释掉了。

  • 默认分支以 side-by-side 模式加载两个量化二进制文件副本,并在两个单独的线程中运行它们。
//Multi-instance threading
var t1 = new Thread(x => Run((bool)x));
t1.Start(false);
var t2 = new Thread(x => Run((bool)x));
t2.Start(false);

//Wait
t1.Join();
t2.Join();
  • 取消注释以下内容以运行标准的 P-Invoke 单实例方法。
//For comparative purposes
PInvoke();
return;
  • 取消注释以下内容以运行单例方法,以便于调试。
//Singleton for easy development
Run(bSingleInstance: true);
return;

请注意,后一种模式在进行更改时效果最好,因为 side-by-side 模式要求将 NativeAPI.DLL 或 Utils.DLL 的任何更改手动复制到 Debug 的 1 和 2 子文件夹中。

参考文献

Financial Numerical Recipes

SAFEARRAY 原生结构体

VARIANT 原生结构体

BSTR 原生类型

历史

  • 1.0 - 首次撰写。
© . All rights reserved.