Hello World
Spiga

在.NET平台上使用Scala语言(下):分析

2009-12-21 00:30 by 老赵, 5395 visits

上一篇文章里我们简单尝试了在Scala里编写.NET应用程序。这个过程并不困难,因为似乎Scala官方已经对此已经有较好的支持了。我们要做的只是“获取工具”,“编译成IL”,最后再“生成程序集”即可。那么,这些工具究竟做了些什么,Scala究竟又是如何支持.NET平台的,它的可用性究竟如何,我们还需要进一步的分析及尝试。

现在看第一个问题。我们知道从Scala源代码生成IL文件的脚本是scalac-net.bat。如果需要了解它做的事情,最直接的方法莫过于查看其中的内容。如果要看明白它的代码,可能需要我们对cmd命令有些了解——不过我也只是略知一二罢了,如果您对其了解不多其实也没有太大关系。经过合理推测,我们知道scalac-net.bat本身不会有什么功能,它只是调用编译器而已。因此,这个脚本文件的职责,无非是收集参数并执行编译器。于是我们打开scalac-net.bat,在众多for/if之中可以发现它最后执行了这样一个命令:

%_JAVACMD% -Xbootclasspath/a:"%_BOOT_CLASSPATH%" %_JAVA_OPTS% %_PROPS% -cp "%_EXTENSION_CLASSPATH%" scala.tools.nsc.Main -target:msil %_ARGS%

那么我们再调用scalac-net.bat的时候这行命令究竟是什么呢?对于此类问题,我们可以再它前面加上ECHO命令,即:

ECHO %_JAVACMD% -Xbootclasspath/a:"%_BOOT_CLASSPATH%" %_JAVA_OPTS% %_PROPS% -cp "%_EXTENSION_CLASSPATH%" scala.tools.nsc.Main -target:msil %_ARGS%

ECHO可以视为cmd的print命令,我们可以用它来观察和学习脚本。再次运行,便可以看到编译器的调用方式了:

D:\scala-2.7.7.final\code> ..\bin\scalac-net.bat test.scala
java -Xbootclasspath/a:"D:\SCALA-~1.FIN\bin\..\lib\scala-library.jar" -Xmx256M -Xms16M -Dscala.home="D:\SCALA-~1.FIN\bin\.." -Denv.classpath="" -Dmsil.libpath="D:\SCALA-~1.FIN\bin\..\lib\predef.dll;D:\SCALA-~1.FIN\bin\..\lib\scalaruntime.dll;D:\SCALA-~1.FIN\bin\..\lib\mscorlib.dll" -Dmsil.ilasm="c:\Windows\Microsoft.NET\Framework\v2.0.50727\ilasm.exe"  -cp "D:\SCALA-~1.FIN\bin\..\lib\mscorlib.dll;D:\SCALA-~1.FIN\bin\..\lib\predef.dll;D:\SCALA-~1.FIN\bin\..\lib\sbaz-tests.jar;D:\SCALA-~1.FIN\bin\..\lib\sbaz.jar;D:\SCALA-~1.FIN\bin\..\lib\scala-compiler.jar;D:\SCALA-~1.FIN\bin\..\lib\scala-dbc.jar;D:\SCALA-~1.FIN\bin\..\lib\scala-library.jar;D:\SCALA-~1.FIN\bin\..\lib\scala-swing.jar;D:\SCALA-~1.FIN\bin\..\lib\scalaruntime.dll" scala.tools.nsc.Main -target:msil test.scala

可以看出,这是在运行一个java程序,并且提供了很多参数。不过参数很多,内容也很乱。不过乱的原因在于其中对于各式命令或者库文件的引用都使用的完整路径。经过换行,相对路径调整,并去除一些明显无用的参数内容(如-cp,即classpath里的dll文件),我们发现其实大约这样的:

D:\scala-2.7.7.final\code> ..\bin\scalac-net.bat test.scala
java
  -Xbootclasspath/a:"..\lib\scala-library.jar"
  -Xmx256M
  -Xms16M
  -Dscala.home=".."
  -Denv.classpath=""
  -Dmsil.libpath="..\lib\predef.dll;..\lib\scalaruntime.dll;..\lib\mscorlib.dll"
  -Dmsil.ilasm="c:\Windows\Microsoft.NET\Framework\v2.0.50727\ilasm.exe"
  -cp "..\lib\sbaz-tests.jar;..\lib\sbaz.jar;..\lib\scala-compiler.jar;..\lib\scala-dbc.jar;..\lib\scala-library.jar;..\lib\scala-swing.jar;"
  scala.tools.nsc.Main
  -target:msil
  test.scala

您可以执行整理后的命令,效果一致。经过一番摸索,再配合scalac.bat -help的输出,我们可以观察出命令的具体意义,例如:

  • Scala编译器其实是一个Java程序,入口是scala.tools.nsc.Main
  • -Dmsil.libpath表明编译时所引用的.NET程序集。
  • -Dmsil.ilasm表明ilasm.exe文件的路径,如果需要直接生成程序集则需要进行指定。

那么假设我们已经编译生成了一个test.exe文件,现在使用.NET Reflector来观察它的信息:

可见test.exe依赖另外三个程序集,它们按照依赖关系分别是:

  1. mscorlib.dll:定义了一个程序的基础需求。
  2. scalaruntime.dll:依赖mscorlib.dll,定义了Scala语言中的各种基础类型。
  3. predef.dll:依赖mscorlib.dll及scalaruntime.dll,定义了scala的基础类库。

看上去并没有什么问题,不是吗?但是,经过简单的思考,似乎又不是那么一回事情。好比,您是否觉得一个Scala程序的依赖实在少了一些?例如您平时写程序时能否仅仅依赖mscorlib.dll,而不使用System.dll或System.Core.dll等其他程序集?那么,为什么Scala便可以仅仅基于mscorlib.dll而构建predef.dll呢?为此,我们简单比较一下predef.dll与Java平台上Scala的标准库——scala-library.jar。首先是predef.dll:

其次是scala-library.jar中的定义:

可以看出,Scala标准库中定义了比predef.dll中更多的类库。例如Scala一直引以为傲的Actor类库,即scala.actors命名空间。换句话说,.NET平台上的Scala并不支持Java平台上的许多高级功能——这样似乎可以理解为什么它只需依赖mscorlib.dll就足够了。不过“标准类库少”是坏事还是好事倒也不能轻易下结论。

如果说这是坏事——类库少自然是坏事。那么“好事”又从何谈起呢?我的理解是:Scala毕竟是为Java平台设计的语言,它本可不必对.NET提供支持。也就是说,.NET平台只是Scala的“副业”。如果说,因为IL和Java Code相近(或者说有很大程度的“包含”关系),那么编译器在写起来相对问题不大,但“类库”就无法讨巧了。如果.NET类库跟得太紧,那么我反而要怀疑它的质量是否成熟。在使用Scala时,我主要关注的其实是“编译器”及最终生成的IL,我并没有期望能够使用Scala在.NET平台上编写程序。对此,编译器是否成熟对我们来说可能更加重要。因此,.NET类库少也并不是坏事——毕竟.NET Framework已经提供了足够的功能,不是吗?

是吗?

如果您比较心细,您应该已经从第一幅图中看出问题来了。我特意将焦点放在mscorlib.dll上,目的便是展示它的版本信息,即:

mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

如果您关注一下平时写程序时所使用的mscorlib.dll,会发现它是这样的:

mscorlib, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e

为什么会不一样?那是因为Scala所使用的mscorlib.dll是“自己带来的”,并不是系统安装的.NET Framework。那么它究竟是什么呢?展开后便可一目了然:

因为它并不是微软提供的.NET Framework,而是Mono平台提供的.NET类库!如果您使用.NET Reflector来查看其中某些类库的具体实现,会发现它和.NET Framework中实现有很明显的不同(如字符串的连接操作)。

有朋友可能会想,这问题应该不大,只要在编译时提供机器上安装的程序集不就可以了吗?但问题是,微软发布的.NET Framework,他们都是依赖于mscorlib.dll——这是每个.NET程序的核心,例如其中定义了一些基础数据类型。想象一下,Scala编译器使用的是Mono里定义的String类型,那么如何把它传递给MS .NET里定义的方法呢?要知道后者使用的可是MS .NET里的String!

经过多番尝试,我无法让Scala编译器使用MS .NET里的程序集——即便是再简单的case。当然,目前我还无法确定这是Scala编译器的问题,亦或的确只是类库的关系。不知道修改一下Scala的编译器或是基于Mono进行编译能否成功,我会再进行更多尝试——如果某一天您发现我又写了一篇“下”,而现在这篇变成了“中”……也是非常正常的事情。:)

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

20 条回复

  1. 老赵
    admin
    链接

    老赵 2009-12-21 09:10:00

    用mono也有个问题,就是这版本(包括强命名)……其实mono的版本应该也是和MS .NET保持一致的……

  2. 徐少侠
    *.*.*.*
    链接

    徐少侠 2009-12-21 09:10:00

    希望还有下
    这篇先当 中 看

  3. JimLiu
    *.*.*.*
    链接

    JimLiu 2009-12-21 09:36:00

    Jeffrey Zhao:用mono也有个问题,就是这版本(包括强命名)……其实mono的版本应该也是和MS .NET保持一致的……


    Base Class Library的命名有不一致的吗?那就头疼了……

  4. 老赵
    admin
    链接

    老赵 2009-12-21 09:44:00

    @JimLiu
    应该不会,主要是全命名不同,如版本号。

  5. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-12-21 10:32:00

    我明白问题所在了。。。。。我一直想,把那个mscorlib换了不就行了么?但问题显然没这么简单。因为另外两个scala的类库对它有依赖关系,看起来只能再将这另外两个scala类库反编译,修改他们的依赖关系到正宗的mscorlib才行。。。。

    那么.NET早期所宣称的Side-by-Side(并行执行)特性能能不能解决这个问题呢?

  6. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-12-21 10:43:00

  7. 刘一凡
    *.*.*.*
    链接

    刘一凡 2009-12-21 10:52:00

    看不懂的新手路过。。。

  8. 老赵
    admin
    链接

    老赵 2009-12-21 11:47:00

    @Ivony...
    看不懂文档里写的什么鸟中文,有机会试试看assemblyBinding……

  9. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-12-21 12:04:00

    Jeffrey Zhao:
    @Ivony...
    看不懂文档里写的什么鸟中文,有机会试试看assemblyBinding……




    见鬼了,我这里反编译scalaruntime.dll,发现其依赖的是正宗的.NET Framework 2.0的mscorlib?!

  10. 老赵
    admin
    链接

    老赵 2009-12-21 12:10:00

    @Ivony...
    靠,我也看到了。
    那为什么predef.dll和test.exe都是依赖mono的阿?类库主要还是predef里定义的啊

  11. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-12-21 13:16:00

    偶发现predef.dll原来是一个控制台程序。。。。。

  12. kisskiki[未注册用户]
    *.*.*.*
    链接

    kisskiki[未注册用户] 2009-12-21 15:52:00

    老赵,在你的qa里面本来已经问了,但是估计问题太多,你米看到,借这篇文章请教下你
    ref:
    hi 老赵,我在ecma335文档上(下载地址:http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-335.pdf)的148页看到这段描述,觉得和自己在实践中有点出入,也许是自己理解有误吧,想请老赵帮忙解答下,描述如下:

    | NativeType ‘[’ ‘+’ Int32 ‘]’

    Array of NativeType with runtime supplied
    element size. The Int32 specifies a parameter to
    the current method (counting from parameter
    number 0) that, at runtime, will contain the size
    of an element of the array in bytes. Can only be
    applied to methods, not fields.
    ------这是描述

    .method int32 M2( int32 marshal(int32), bool[] marshal(bool[+1]) )
    Method M2 takes two arguments: an int32, and an array of bools: the number of elements in that array is
    given by the value of the first parameter.
    ---这是个例子

    按我理解就是+后数字代表的是方法的第几个参数指明了数组的元素个数,由序数0开始,这里我就有个问题既然从序数0开始,那+1不是指第二个参数不就是数组本身?

  13. 老赵
    admin
    链接

    老赵 2009-12-21 17:21:00

    @Ivony...
    Java其实都是控制台程序,因为其实它们都是jar包,只是执行不同的方法作为入口而已,参数传给java.exe就行了。

  14. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-12-22 04:34:00

    kisskiki:
    .method int32 M2( int32 marshal(int32), bool[] marshal(bool[+1]) )

    按我理解就是+后数字代表的是方法的第几个参数指明了数组的元素个数,由序数0开始,这里我就有个问题既然从序数0开始,那+1不是指第二个参数不就是数组本身?


    呵呵,如果那个例子写的是:
    .method static int32 M2( int32 marshal(int32), bool[] marshal(bool[+1]) )
    那你说的没错,+1会是指那个bool数组。

    但原本的例子里没有static修饰符,意味着它是成员方法,也就是说在第一位有一个隐藏参数(“this”)。算上this的话,int32那个参数就变成+1位置上的了。可以参考ECMA-335的8.4.2、8.11.1、12.4.1.4等小节。

    ——越俎代庖了 =_=|||

  15. 老赵
    admin
    链接

    老赵 2009-12-22 08:33:00

    @RednaxelaFX
    哇……

  16. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-12-22 09:47:00

    RednaxelaFX:

    kisskiki:
    .method int32 M2( int32 marshal(int32), bool[] marshal(bool[+1]) )

    按我理解就是+后数字代表的是方法的第几个参数指明了数组的元素个数,由序数0开始,这里我就有个问题既然从序数0开始,那+1不是指第二个参数不就是数组本身?


    呵呵,如果那个例子写的是:
    .method static int32 M2( int32 marshal(int32), bool[] marshal(bool[+1]) )
    那你说的没错,+1会是指那个bool数组。

    但原本的例子里没有static修饰符,意味着它是成员方法,也就是说在第一位有一个隐藏参数(“this”)。算上this的话,int32那个参数就变成+1位置上的了。可以参考ECMA-335的8.4.2、8.11.1、12.4.1.4等小节。

    ——越俎代庖了 =_=|||




    我刚猜到,然后,就看到了。。。。

  17. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-12-22 09:51:00

    还是回到这篇文章的问题上来吧,似乎不好解决,就算是程序集重定向似乎也没有办法解决编译的问题。何况这个问题牵涉到编译器对基元类型的引用,不从编译器入手似乎没法解决。

    简单的说,编译器看到"abc"这样的东西的时候,要把它当作什么类型,显然在这里scala的il编译器把它当作了MONO的String类型,所以将任何这样的基元类型传递给.NET的方法,例如我用HttpUtility.UrlEncode来做实验:
    HttpUtility.UrlEncode( "Hello World!" );
    会是编译错误。。。。。。


    当然还有其他的编译器内部错误,但那些问题都解决了这个也绕不过去。

  18. 老赵
    admin
    链接

    老赵 2009-12-22 09:54:00

    @Ivony...
    所以还是修改编译器吧……不过IL其实在代码表现上也不分版本的不是吗?
    就是说,在IL文件里表示一个String,是不强命名的,mscorlib是在前面指定的。

  19. 老赵
    admin
    链接

    老赵 2009-12-22 11:29:00

    @Ivony...
    话说我有一个猜想。
    mono其实就是个跨平台的.net运行环境,不是吗?
    那么我们可以把它拿到windows上来代替MS的实现,应该可以吧。
    关键是,mono是开源的,我们可以自己修改……

  20. 银河
    *.*.*.*
    链接

    银河 2009-12-22 16:57:00

    @Jeffrey Zhao
    关于在 mono 上运行 Scala,我刚才写了一篇文章:也谈在 .NET 平台上使用 Scala 语言(上)

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我