乐者为王

Do one thing, and do it well.

VC++动态链接库(DLL)编程入门

DLL是一个包含可由多个程序同时使用的代码和数据的集合。例如,在Windows操作系统中,Comdlg32动态链接库 执行与对话框有关的常见函数。因此,每个程序都可以使用该DLL中包含的功能来实现“打开”对话框。这有助于促进代码复用和内存的有效使用。通过使用DLL,程序可以实现模块化,由相对独立的组件组成。例如,一个计帐程序可以按模块来销售。可以在运行时将各个模块加载到主程序中(如果安装了相应模块)。因为模块是彼此独立的,所以程序的加载速度更快,而且模块只在相应的功能被请求时才加载。

DLL的优点

  1. 使用较少的资源。当多个程序使用同一个函数库时,DLL可以减少在磁盘和物理内存中加载的代码的重复量。这不仅可以大大影响在前台运行的程序,而且可以大大影响其它在Windows操作系统上运行的程序;
  2. 简化部署和安装。当DLL中的函数需要更新或修复时,只要函数的参数和返回值没有更改,就不需重新编译或重新建立程序与该DLL的链接。此外,如果多个程序使用同一个DLL,那么多个程序都将从该更新或修复中获益;
  3. 支持多语言程序。只要程序遵循函数的调用约定,用不同编程语言编写的程序就可以调用相同的DLL函数。程序与DLL函数在下列方面必须是兼容的:函数期望其参数被推送到堆栈上的顺序,是函数还是应用程序负责清理堆栈,以及寄存器中是否传递了任何参数;
  4. 使国际版本的创建轻松完成。通过将资源放到DLL中,创建应用程序的国际版本变得容易得多。可将用于应用程序的每个语言版本的字符串放到单独的DLL资源文件中,并使不同的语言版本加载合适的资源。

DLL的类型(Kinds of DLLs)

Visual C++支持三种类型的DLL,它们分别是Non-MFC DLL、MFC Regular DLL和MFC Extension DLL:

  1. Non-MFC DLL指的是不用MFC的类库结构,直接用C语言写的DLL,其导出的函数是标准的C接口,能被MFC或非MFC编写的客户程序调用;
  2. Extension DLL支持C++接口,也就是说它导出C++函数或者整个类给客户程序。导出函数可以使用C++或MFC的数据形式作为参数或返回值,当导出整个类时,客户程序可以创建此类的对象或者从这些类进行派生。使用Extension DLL的一个问题就是该DLL仅能和MFC客户程序一起工作;
  3. Regular DLL和上述的Extension Dll一样,也是用MFC类库编写的,它的一个明显的特点是在源文件里有一个继承CWinApp的类(注意:此类DLL虽然从CWinApp派生,但没有消息循环)。Regular DLL有一个很大的限制就是,它只可以导出C风格的函数,但不能导出C++类、成员函数或重载函数。调用Regular DLL的客户程序不必是MFC应用程序,也可以是在Visual C++、Dephi、Visual Basic等环境下开发的客户程序。

DLL的加载

客户程序使用DLL的方式有两种:隐式链接(静态加载或加载时动态链接)和显式链接(动态加载或运行时动态链接)。

为了隐式链接到DLL,客户程序必须从DLL的提供程序获取下列项:

  1. 包含导出函数或C++类声明的头文件(.h文件);
  2. 要链接的导入库(.lib文件);
  3. 实际的DLL(.dll文件)。

使用DLL的客户程序必须include头文件(此头文件包含每个DLL中的导出函数或C++类),并且链接到此DLL的创建者所提供的导入库。

1
2
3
4
5
// Cacl.cpp
DLLAPI int Sum(int a, int b)
{
    return a + b;
}
1
2
3
4
5
6
7
8
// Cacl.h
#ifdef CACL_EXPORTS
#define DLLAPI __declspec(dllexport)
#else
#define DLLAPI __declspec(dllimport)
#endif

DLLAPI int Sum(int a, int b);
1
2
// Client.cpp
DLLAPI int Sum(int a, int b);

为了显式链接到DLL,客户程序必须在运行时通过函数调显式加载DLL:

  1. 调用LoadLibrary加载DLL和获取模块句柄;
  2. 调用GetProcAddress获取指向客户程序要调用的每个导出函数的函数指针(由于客户程序是通过指针调用DLL的函数,编译器不生成外部引用,故无需与导入库链接);
  3. 使用完DLL后调用FreeLibrary释放资源。
1
2
3
4
5
6
7
8
9
10
11
12
// Client.cpp
HINSTANCE hDLL = LoadLibrary("demo");
if (hDLL != NULL)
{
    LPFNDLLFUNC1 lpfnDllFunc1 = GetProcAddress(hDLL, "Sum");
    if (lpfnDllFunc1 == NULL)
    {
        FreeLibrary(hDLL);
        return SOME_ERROR_CODE;
    }
    return lpfnDllFunc1(dwParam, uParam);
}

客户程序如何找到DLL

如果用LoadLibrary显式链接DLL的话,可以指定DLL的全路径名。如果没有指定路径名,或者使用了隐式链接,则系统将使用下面的搜索序列来定位DLL:

  1. 包含客户可执行文件所在的目录;
  2. 当前目录;
  3. Windows系统目录(可用GetSystemDirectory函数获取);
  4. Windows目录(可用GetWindowsDirectory函数获取);
  5. 在Path环境变量里列出的目录(注意:不使用LIBPATH环境变量)。

导出DLL函数

DLL文件的布局与EXE文件非常相似,但有一个重要差异:DLL文件包含导出表。导出表包含DLL导出到客户程序的每个函数的名称。只有导出表中的函数可由客户程序访问。DLL中的任何其它函数都是DLL私有的。通过使用带/EXPORTS选项的dumpbin工具,可以查看DLL的导出表。有两种方法可以从DLL导出函数:

方法1:在定义中使用__declspec(dllexport)关键词。

1
2
3
4
5
6
7
// 导出函数
__declspec(dllexport) void __cdecl Function();

// 导出类中的所有公共数据成员和成员函数
class __declspec(dllexport) CExampleExport : public CObject
{
}

方法2:在生成DLL时,创建一个后缀名为def的模块定义文件(如果希望按序号而不是按名称从DLL导出函数,则使用此方法)。

实例DLL和客户程序代码:

1
2
3
4
5
6
7
8
// Cacl.h
#ifdef CACL_EXPORTS
#define DLLAPI __declspec(dllexport)
#else
#define DLLAPI __declspec(dllimport)
#endif

DLLAPI int Sum(int a, int b);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Cacl.cpp
#include <windows.h>
#include "Cacl.h"

BOOL APIENTRY DllMain(HANDLE hModule, DWORD reason, LPVOID lpReserved)
{
    switch (reason)
    {
        case DLL_PROCESS_ATTACH:
        case DLL_THREAD_ATTACH:
        case DLL_THREAD_DETACH:
        case DLL_PROCESS_DETACH:
            break;
    }
    return TRUE;
}

DLLAPI int Sum(int a, int b)
{
    return a + b;
}
1
2
3
4
5
6
7
8
9
// Client.cpp
#include <stdio.h>
#include "Cacl.h"

int main(int argc, char* argv[])
{
    printf("Sum = %d\n", Sum(5, 3));
    return 0;
}

如何调试DLL

调试DLL很容易,只要从DLL工程启动调试器即可。第一次这样做的时候,调试器会请求给出客户EXE程序的路径。之后,每次从调试器运行DLL时,调试器会自动装入客户EXE程序,而EXE用搜索序列找到DLL。

批量更新表中某个字段的值到另一个字段的存储过程(MySQL版)

对博客做了一些修改,需要将entries表中的updatetime字段值替换成pubtime字段的值,为此写了一个MySQL存储过程,其中涉及游标的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
DELIMITER //

CREATE PROCEDURE sp_batch_replace_field_value_with_another()
BEGIN
    DECLARE p_id INT;
    DECLARE p_pubtime DATETIME;
    DECLARE p_done BOOL;
    DECLARE p_cursor CURSOR FOR SELECT id, pubtime FROM entries;
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET p_done = true;

    OPEN p_cursor;
        REPEAT
            FETCH p_cursor INTO p_id, p_pubtime;
            UPDATE entries SET updatetime = p_pubtime WHERE id = p_id;
            UNTIL p_done = true
        END REPEAT;
    CLOSE p_cursor;
END//

DELIMITER ;

第一次在SQLyog中写存储过程,在修改了存储过程后竟然不知道如何保存。点击File -> Save却是保存为另外一个文件,而不是直接保存修改的存储过程。经过多番尝试后发现按F5键就可以了。

com.sybase.jdbc.SybSQLException: The parameter '@user' in the procedure 'sp_adm_chk_lvl' was not declared as an OUTPUT parameter

运行环境:WebLogic 8.16 + Sybase ASE 12.5

问题:The New, Save from File menu are grayed out

分析打印出来的日志文件找到有以下异常出现:

1
2
3
4
5
org.springframework.jdbc.UncategorizedSQLException: CallableStatementCallback
    ; uncategorized SQLException for SQL [{? = call sp_adm_chk_lvl(?, ?, ?, ?, ?, ?, ?)}]
    ; SQL state [ZZZZZ]; error code [276]
    ; The parameter '@user' in the procedure 'sp_adm_chk_lvl' was not declared as an OUTPUT parameter.
    ; nested exception is com.sybase.jdbc.SybSQLException: The parameter '@user' in the procedure 'sp_adm_chk_lvl' was not declared as an OUTPUT parameter.

搜索到一篇出现类似问题的文章 http://www.ibm.com/support/docview.wss?uid=swg21174150 ,其中有这么一段文字:

1
2
3
4
If you call CallableStatement.clearParameters() when the statement has a ?=rpc return value,
we forget that there is a return value and the next time the rpc is executed we send too many
input parameters, and they are all shifted to the right. This problem has been corrected,
and jConnect now has the expected behavior for the clearParameters() method.

因此猜测菜单变灰的问题是由于使用了不正确的JDBC Driver版本。在Weblogic Console发现Connection Pools中Driver Name是com.sybase.jdbc.SybDriver,而正确的设置应该是com.sybase.jdbc2.jdbc.SybDriver,修改Driver Name后New, Save菜单项果然可以正常使用了。

双色球(Union Lotto)模拟摇奖器

双色球基本规则:

  1. 彩票投注区分为红色球号码区和蓝色球号码区;
  2. 每注投注号码由6个红色球号码和1个蓝色球号码组成。红色球号码从1-33中选择;蓝色球号码从1-16中选择;
  3. 摇奖时先摇出6个红色球号码,再摇出1个蓝色球号码,摇出的红色球号码按从小到大的顺序和蓝色球号码一起公布。

双色球模拟摇奖器代码:

1
2
3
4
5
6
7
# union_lotto.rb
red_balls = Array.new(33) {|i| i + 1}
blue_balls = Array.new(16) {|i| i + 1}
1.upto(6) do
  print red_balls.delete_at(rand(red_balls.length)), ' '
end
puts '- ' + blue_balls.delete_at(rand(blue_balls.length)).to_s

Cannot delete or update a parent row: a foreign key constraint fails

在SQLyog中Import Data或Empty Table时,出现:

1
2
Error No. 1451
Cannot delete or update a parent row: a foreign key constraint fails

查看官方文档Setting FOREIGN_KEY_CHECKS to 0 can also be useful for ignoring foreign key constraints during LOAD DATA and ALTER TABLE operations.知道这可能是MySQL在InnoDB中设置了foreign key关联,造成无法更新或删除数据。可以通过设置FOREIGN_KEY_CHECKS变量来避免这种情况。

1
SET FOREIGN_KEY_CHECKS = 0;

JavaScript中的undefined

JavaScript有6种类型,undefined、null、boolean、number、string和object。其中,null类型只有一个值null。undefined类型也只有一个值,即undefined,当声明的变量没有被赋值的时候,该变量的默认值就是undefined。

1
var foo;

上面这行代码声明了变量foo,没有赋值。脚本引擎在处理代码的时候会给该变量赋予值undefined。实际上,该行代码等价于

1
var foo = undefined;

可以用下面的代码测试:

1
2
3
4
var foo;
alert(foo == undefined);    // true
alert(foo === undefined);    // true
alert(typeof(foo));    // undefined

JavaScript另一个有趣的方面是在使用变量之前不必声明。例如:

1
2
3
var foo = "Hello ";
bar = foo + "world!";
alert(bar);    // Hello world!

变量bar并没有用var运算符定义,这里只是使用了它,就像已经声明过它一样。脚本引擎遇到未声明过的标识符时,用该变量名创建一个全局变量,并将其初始化为指定的值(这里是foo + "world")。这是该语言的便利之处,不过如果不能紧密跟踪变量,这样做也很危险。考虑下面的代码:

1
2
3
4
5
var foo;
// Make sure this variable isn't defined
//var bar;
alert(foo);
alert(bar);

执行的结果是:

undefined

undefined-error

alert(bar)出现错误是因为bar被创建成全局变量后没有被初始化,还处在未初始化(uninitialized)状态。但是,typeof运算符并不真正区分这两种值。考虑下面的代码:

1
2
3
4
5
var foo;
// Make sure this variable isn't defined
//var bar;
alert(typeof(foo));    // undefined
alert(typeof(bar));    // undefined

前面的代码对两个变量的输出都是“undefined”,即使变量bar在使用前没有被声明。

当函数无明确返回值时,返回的也是值undefined,如下所示:

1
2
3
function foobar() { }
alert(foobar() == undefined);    // true
alert(foobar() === undefined);    // true

ECMAScript认为undefined是从null派生出来的,所以把它们定义为相等的:

1
alert(null == undefined);    // true

尽管这两个值相等,但它们的含义不同。undefined是声明了变量但未赋值时赋予该变量的值,null则用于表示尚未存在的对象(但从技术上来说,null仍然是原始值)。如果要区分两者,要使用===或typeof运算符。

CSS盒模型

平面示意图

box-model-01

3D示意图(原图出自:hicksdesign)

box-model-02

Flash示意模型

W3C DOM - {setAttribute()}

setAttribute(string name, string value):增加一个指定名称和值的新属性,或者把一个现有的属性设定为指定的值。

setAttribute()的差异

我们经常需要在JavaScript中给Element动态添加各种属性,这可以通过使用setAttribute()来实现,这就涉及到了浏览器的兼容性问题。

1
2
var obj = document.getElementById("obj");
obj.setAttribute("onclick", "javascript:alert('This is a test!');");

这里利用setAttribute指定Element的onclick属性,简单,很好理解。但是IE不支持,IE并不是不支持setAttribute这个函数,而是不支持用setAttribute设置某些属性,例如对象属性、集合属性、事件属性,也就是说用setAttribute设置style和onclick 这些属性在IE中是行不通的。为达到兼容各种浏览器的效果,可以用点符号法来设置Element的对象属性、集合属性和事件属性。

1
2
3
4
document.getElementById("obj").className = "fruit";
document.getElementById("obj").style.cssText = "color: #00f;";
document.getElementById("obj").style.color = "#00f";
document.getElementById("obj").onclick = function () { alert("This is a test!"); }

关于class和className

class属性在W3C DOM中扮演着很重要的角色,但由于浏览器差异性仍然存在。使用setAttribute("class", vName)语句动态设置element的class属性在Firefox中是行的通的,在IE中却不行。因为使用IE内核的浏览器不认识“class”,要改用“className”;同样,Firefox也不认识“className”。所以常用的方法是二者兼备:

1
2
element.setAttribute("class", vName);
element.setAttribute("className", vName);    // for IE

cellspacing和cellpadding

虽然在CSS中存在与cellpadding和cellspacing这两个属性等价的样式属性padding和border-spacing。然而,浏览器对这些样式属性支持的不一致,有时仍会使用cellpadding和cellspacing来调整表格的间距。不过,在Firefox中有效的setAttribute("cellpadding", value)到了IE下就不行了,必须改成cellPadding才可以(注意:p要大写)。幸好Firefox也支持setAttribute("cellPadding", value)。所以二者兼容的代码是:

1
2
element.setAttribute("cellSpacing", value);
element.setAttribute("cellPadding", value);

JSON in JavaScript

英文原文:http://www.json.org/js.html

JavaScript是一门通用编程语言,被作为页面脚本语言引入Netscape Navigator。它仍被广泛地认为是Java的一个子集,但它不是。它是一门有着类C语法soft objects的类Scheme语言,JavaScript在ECMAScript Language Specification, Third Edition中被标准化。

JSON是JavaScript对象字面量表示法(object literal notation)的一个子集。因为JSON是JavaScript的一个子集,所以它可以毫不费力地被用在这门语言中。

1
2
3
4
5
6
var myJSONObject = {"bindings": [
        {"ircEvent": "PRIVMSG", "method": "newURI", "regex": "^http://.*"},
        {"ircEvent": "PRIVMSG", "method": "deleteURI", "regex": "^delete.*"},
        {"ircEvent": "PRIVMSG", "method": "randomURI", "regex": "^random.*"}
    ]
};

在这个例子中,一个含有单个成员“bindings”的对象被创建,该成员包含一个含有三个对象的数组,每个对象含有“ircEvent”,“method”和“regex”三个成员。

成员可以通过点或下标运算符检索。

1
myJSONObject.bindings[0].method    // "newURI"

你可以使用eval函数将一段JSON文本转换成一个对象,eval函数会调用JavaScript编译器。因为JSON是JavaScript的一个真子集(a proper subset),编译器将正确地解析文本并产生一个对象结构。文本必须被包裹在括号内,以免在JavaScript的语法中的歧义上犯错误。

1
var myObject = eval('(' + myJSONtext + ')');

eval函数是非常快的。然而,它可以编译和执行任何JavaScript程序,所以可能会有安全问题。当源代码是可信的与合格的的时候才可以使用eval函数。使用JSON语法分析器则安全的多。在基于XMLHttpRequest的Web应用中,通讯只被允许朝着提供该页面的同源的方向,所以它是可信的。但它可能不是合格的。如果服务器在它的JSON编码里不是严格的,或者它没有严谨地验证所有的输入,然后它可能交付无效的JSON文本,它们可能携带危险的脚本。eval函数将会执行这些脚本,释放恶意。

为抵御这些,应该使用JSON语法分析器。JSON语法分析器将只识别JSON文本,拒绝所有的脚本。在提供本地JSON支持的浏览器里,JSON语法分析器也比eval快的多。预计本地JSON支持将会被包含在下一个ECMAScript标准中。

1
var myObject = JSON.parse(myJSONtext, reviver);

可选的reviver参数是一个函数,它会被最终结果的各个层面上的每个键和值调用。每个值会被reviver函数的返回结果替换。这可以被用来将通用对象改编成为伪类实例,或者被用来将date字符串转化成Date对象。

1
2
3
4
5
6
7
8
9
10
myData = JSON.parse(text, function (key, value) {
    var type;
    if (value && typeof value === 'object') {
        type = value.type;
        if (typeof type === 'string' && typeof window[type] === 'function') {
            return new (window[type])(value);
        }
    }
    return value;
});

JSON stringifier则作相反的工作,它将JavaScript数据结构转换为JSON文本。JSON不支持循环的数据结构,所以注意不要把循环的结构交给JSON stringifier。

1
var myJSONText = JSON.stringify(myObject, replacer);

如果stringify方法看到一个对象包含toJSON方法,它会调用该方法,并字符串化返回的值。这允许一个对象去确定其自己的JSON表示。

stringify~~原文是stringifier,疑似拼错~~方法可以接受一个可选的字符串数组。这些字符串被用于选取那些将会被包含在JSON文本中的属性。

stringify~~原文是stringifier,疑似拼错~~方法可以接受一个可选的replacer函数。该函数会在结构中每个值的toJSON方法(如果有)之后被调用。它将每个键和值作为参数传递,并且this将会被绑定到持有键的对象上。返回的值会被字符串化。

没有在JSON中表示的值(例如函数和undefined)被排除在外。

非有限的数字被替换为null。要替换成其它的值,你可以使用像这样的replacer函数:

1
2
3
4
5
6
function replacer(key, value) {
    if (typeof value === 'number' && !isFinite(value)) {
        return String(value);
    }
    return value;
}

给JSON.parse一个相应的reviver可以还原这些。

The open source code of a JSON parser and JSON stringifier is available. When minified it is less than 2.5K.

集成P6Spy到Spring后没有产生日志文件的问题

网上关于集成P6Spy到Spring的帖子说:在Spring的配置文件中添加:

1
2
3
<bean id="dataSource" class="com.p6spy.engine.spy.P6DataSource" destroy-method="close">
    <constructor-arg ref="dataSourceTarget" />
</bean>

然后将原来dataSource定义的id值改为dataSourceTarget:

1
2
3
4
5
6
<bean id="dataSourceTarget" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="${driver}" />
    <property name="url" value="${url}" />
    <property name="username" value="${username}" />
    <property name="password" value="${password}" />
</bean>

但是照着这样做了以后却发现没有生成日志文件。

下载P6Spy的源代码进行阅读后找到了问题所在。打开P6Util.java文件,在classPathFile()方法中有这样一条语句:

1
fp = classLoadPropertyFile(Thread.currentThread().getContextClassLoader().getResource(file))

getResource(file)用来读取spy.properties配置文件,file的值就是spy.properties的文件路径。我的syp.properties放在了C:\Tomcat 5.5\webapps\netlink\WEB-INF\classes目录下,所以它返回的值是C:/Tomcat%205.5/webapps/netlink/WEB-INF/classes/spy.properties,Tomcat 5.5中的空格被编码成了%20。然后在classLoadPropertyFile()方法中就直接使用该值来创建File对象。可想而知,肯定创建失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
public static File classLoadPropertyFile(java.net.URL purl) {
    try {
        if (purl != null) {
            // modified by jayakumar for JDK 1.2 support
            //return new File(purl.getPath());
            return new File(getPath(purl));
            // end of modification
        }
    } catch (Exception e) {
        // we ignore this, since JDK 1.2 does not suppport this method
    }
    return null;
}

在classPathFile()方法中通过判断fp.exists()来决定是否返回日志文件的路径,既然上面创建File对象失败了,那fp.exists()肯定不可能为true了,日志文件也就不可能被创建了。

解决问题的方法超简单,把Tomcat 5.5中的空格去掉就行了。