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

XSL 代码生成器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (7投票s)

2011年10月4日

CPOL

5分钟阅读

viewsIcon

65410

downloadIcon

1440

一个基于 XML/XSLT 的解决方案,用于自动生成 API 和代码。

引言

在某个发展阶段(无论是职业还是心态),你可能会在自动生成代码的领域中寻求庇护。

本文的目标是为你提供创建自定义、通用、自制代码生成器的工具。它将能够根据你的选择,在一到多种语言中生成接口、数据对象(甚至在需要时生成具体代码),并且可以在支持 XML/XSL 的任何平台上运行。

目标受众(技术方面)

本文的目标读者是具备足够的 XML 和 XSLT/XPATH 知识的开发者。文章中提供了一个简短的 C# 程序,并且下面示例的输出将是 C# 和 C++。

工作原理

提出的解决方案利用 XML 作为 API 输入,并配有相应的 XSL 转换器。将两者结合使用第三方组件,即可生成所需输出。这使我们能够完全控制生成的产品。

撸起袖子加油干

在我们深入之前,我先说明一下,这里提供的示例是不完整的。演示项目包含了完整的代码以及一个帮助生成输出文件的批处理文件。

让我们开始吧。

动手实践

在接下来的示例中,我们将定义一个简单的 RemoteControl API。

示例 #1

让我们从 PlayStop 功能开始。

输入的 XML 将是

<class name="RemoteControl">
    <method name="Play"/>
    <method name="Stop"/>
</class>

如果我们查看 C# 代码生成器的输出,我们希望看到类似这样的内容

public interface IRemoteControl
{
  void Play();
  void Stop();
}

现在,让我们看看 XSL 转换器——这才是魔法发生的地方

<xsl:template match="class">
public interface I<xsl:value-of select="@name"/>
{
<xsl:for-each select="method">
    void <xsl:value-of select="@name"/>();
</xsl:for-each>
}
</xsl:template>
细则

这很简单,我猜。对于输入 XML 中的每个 class 节点,输出类,迭代其所有方法,并在它们的一侧加上硬编码的 void,另一侧加上 ();

示例 #2

使用相同的 XML 输入,让我们重写 XSL 转换器以同时生成 C# 和 C++ 接口

<!-- MAIN -->
<xsl:template match="/">
    <xsl:apply-templates select="class" mode="csharp"/>
    <xsl:apply-templates select="class" mode="cpp"/>
</xsl:template>

<!-- C# -->
<xsl:template match="class" mode="csharp">
// C#
public interface I<xsl:value-of select="@name"/>
{
<xsl:for-each select="method">
    void <xsl:value-of select="@name"/>();
</xsl:for-each>
}
</xsl:template>

<!-- C++ -->
<xsl:template match="class" mode="cpp">
// C++
class I<xsl:value-of select="@name"/>
{
public:
<xsl:for-each select="method">
    virtual void <xsl:value-of select="@name"/>() = 0;
</xsl:for-each>
};
</xsl:template>
细则

我们从 XML 的根节点 (match="/") 开始,对于每个 class 节点,我们生成其 C# 表示,然后是其 C++ 表示。

在此示例中,mode 属性用作语言类型。当然,也可以将语言类型作为参数传递给 xsl:template,或者使用全局 XSL 值,甚至可以从命令行参数获取,等等。

结果输出
// C#
public interface IRemoteControl
{
    void Play();
    void Stop();
}

// C++
class IRemoteControl
{
public:
    virtual void Play() = 0;
    virtual void Stop() = 0;
};

说起代码生成器,让我们看看这些示例是如何产生的。

输出代码生成器

所以我们有了 XML 和 XSL 输入文件,但我们还需要一个东西来将它们组合在一起并生成输出文件。

这是一个简单的、简短的 C# 程序,可以完成这项工作

class CodeGen
{
    static void Main( string[] args )
    {
        XPathDocument xmlDoc = new XPathDocument( args[0] );

        XslCompiledTransform xslt = new XslCompiledTransform();
        xslt.Load( args[1] );

        XmlTextWriter writer = new XmlTextWriter( args[2], null );
        xslt.Transform( xmlDoc, writer );
    }
}
注释
  1. 语法是:CodeGen.exe <XML 文件名> <XSL 文件名> <输出文件名>
  2. 代码分解很简单:加载 XML 和 XSL 文件,使用 XslCompiledTransform 进行转换,然后将结果写入输出文件。
  3. 我鼓励你了解 XsltArgumentList,它可用于将参数传递给 XslCompiledTransform
  4. 显而易见,但值得一提的是,这个小程序不包含任何形式的错误处理。请随意添加你自己的。

示例 #3

我假设基本思路已经很清楚了。第三个也是最后一个示例稍微复杂/全面一些。它的主要目标是让你更广泛地了解使用这种范式可以实现的目标。

所以,让我们丰富我们的 XML 输入,添加返回值、更多方法以及偶尔的参数

<class name="RemoteControl">
    <method name="Play" retval="void" />
    <method name="Stop" retval="void" />
    <method name="GetTrackNumber" retval="int"/>
    <method name="GetTrackLength" retval="double">
        <param name="deviceNumber" type="int" />
        <param name="trackNumber" type="int" />
    </method>
</class>

现在让我们更新 XSL 转换器。

我们希望输出再次生成 C# 和 C++ 结果。这次,对于每种语言,我们将生成一个接口和一个存根类(例如,用于单元测试)

<!-- MAIN -->
<xsl:template match="/">
    <xsl:apply-templates select="class" mode="csharp"/>
    <xsl:apply-templates select="class" mode="cpp"/>
</xsl:template>

<!-- C# -->
<xsl:template match="class" mode="csharp">
//////////////////////////////////////////
// C# section
public interface I<xsl:value-of select="@name"/>
{
<xsl:for-each select="method">
    <xsl:apply-templates select="."/>;
</xsl:for-each>
}

public class <xsl:value-of select="@name"/>Stub : I<xsl:value-of select="@name"/>
{
<xsl:for-each select="method">
    public <xsl:apply-templates select="."/> { throw new NotImplementedException(); }
</xsl:for-each>
}
</xsl:template>

<!-- C++ -->
<xsl:template match="class" mode="cpp">
//////////////////////////////////////////
// C++ section
class I<xsl:value-of select="@name"/>
{
public:
<xsl:for-each select="method">
    virtual <xsl:apply-templates select="."/> = 0;
</xsl:for-each>
};

class <xsl:value-of select="@name"/>Stub : 
      public I<xsl:value-of select="@name"/>
{
public:
    class NotImplementedException {};

<xsl:for-each select="method">
    virtual <xsl:apply-templates select="."/> { throw new NotImplementedException(); }
</xsl:for-each>
};
</xsl:template>

<!-- Common -->
<xsl:template match="method"><xsl:value-of select="@retval"/>
  <xsl:value-of select="@name"/>(<xsl:apply-templates select="param"/>)
  </xsl:template>

<xsl:template match="param"><xsl:if test="position() > 1">, 
  </xsl:if><xsl:value-of select="@type"/>
  <xsl:value-of select="@name"/></xsl:template>
细则

MAIN 部分负责迭代输入 XML 文件根节点下的所有 class 节点。对于每个节点,它将生成 C# 表示,然后是 C++ 表示。

C#C++ 部分都为给定的 XML 节点生成一个接口和一个存根类。

请注意,这两种语言都使用了 COMMON 部分。这是因为这两种语言中方法签名的核心是相同的(嗯……在现实生活中并非如此,但在本例中是)。一个方法可以有可选的参数。这些参数也将以相同的方式为两种语言进行处理。

结果输出
//////////////////////////////////////////
// C# section
public interface IRemoteControl
{
    void Play();
    void Stop();
    int GetTrackNumber();
    double GetTrackLength(int deviceNumber, int trackNumber);
}

public class RemoteControlStub : IRemoteControl
{
    public void Play() { throw new NotImplementedException(); }
    public void Stop() { throw new NotImplementedException(); }
    public int GetTrackNumber() { throw new NotImplementedException(); }
    public double GetTrackLength(int deviceNumber, 
           int trackNumber) { throw new NotImplementedException(); }
}

//////////////////////////////////////////
// C++ section
class IRemoteControl
{
public:
    virtual void Play() = 0;
    virtual void Stop() = 0;
    virtual int GetTrackNumber() = 0;
    virtual double GetTrackLength(int deviceNumber, int trackNumber) = 0;
};

class RemoteControlStub : public IRemoteControl
{
public:
    class NotImplementedException {};

    virtual void Play() { throw new NotImplementedException(); }
    virtual void Stop() { throw new NotImplementedException(); }
    virtual int GetTrackNumber() { throw new NotImplementedException(); }
    virtual double GetTrackLength(int deviceNumber, 
            int trackNumber) { throw new NotImplementedException(); }
};

接下来呢?

这里的示例演示了开发专有代码生成器的基本概念。

这些概念可以与以下一项或多项相结合进行丰富

  • 自动生成数据结构。这些数据结构也可以由自动生成的接口使用。
    • 一个 .NET 项目可能会使用 .xsd 文件以及 xsd.exe 来完成这项任务。
  • 自动化创建一组输入/输出文件。
    • 你可能希望自动生成一个项目文件(例如,.csproj),其中包含所有自动生成的源代码文件。然后,你可以将此项目作为最终验证阶段的一部分进行构建。
    • 这些构建结果可以用作最终编译的二进制文件。
    • 作为副产品,如果存在编译错误,它们也可以作为错误检测器。
  • XSL 的静态参数(例如,当前时间),甚至是从命令行传递的参数。
  • 可以传递给 XSL 的动态代码对象(仔细查看 XsltArgumentList,即:XsltArgumentList.AddExtensionObject)。
  • 动态脚本,包括即时编译和执行的代码(请参阅 使用 <msxsl:script> 进行 XSLT 样式表脚本编写)。
  • ...等等(取决于你天马行空的想象)。

现在,就看你的技能和创造力来量身定制一个适合你特定需求的解决方案了。我认为很明显——天空才是极限!

历史

  • v1.0 (2011 年 10 月 2 日)。
  • v1.1 (2011 年 10 月 4 日) - 添加了指向演示项目的链接。
© . All rights reserved.