MS RFC 24:MapScript内存管理

日期

2006/12/3

作者

乌姆贝托·尼科莱蒂

联系

umberto.nicoletti@gmail.com

最后编辑

2006/12/31

状态

多恩

版本

MAPServer 5

跟踪器

https://github.com/MapServer/MapServer/issues/2032

跟踪器3.2

https://github.com/MapServer/MapServer/issues/2442

身份证件

$ID$

概述

swig包装机中的内存管理具有困难和容易出错的传统。包装器的程序员必须处理可分配的内存,然后在两个独立的环境中释放:宿主语言,如Java、C语言或Perl和封装的本机代码。

大多数现代语言都实现了垃圾收集,因此开发人员不必关心内存管理。编程语言跟踪内存(或对象,实际上是)分配,当对象超出范围时(不能再从运行的程序访问),它将其标记为符合垃圾回收条件。后台进程偶尔会唤醒并释放与标记对象相关联的内存。有关GC的详细信息,请参阅维基百科词条:https://en.wikipedia.org/wiki/Garbage_collection_(computer_science)

在大多数情况下发生的是,一些内存被分配在Java中,然后通过调用一些被包装的方法指向另一个指针。最终,GC运行并释放内存。一旦其他指针被取消引用,宿主语言就会因为分段错误而崩溃(在UNIX术语中)。

MapServer Swig包装器遇到垃圾收集问题,例如在动态地向地图添加层时。

此RFC的目的是解决这些问题,并提供一个可在MapServer 5.0发布时及时实现的解决方案。

此RFC不涉及线程安全。

问题描述

本节概述了mapscript内存管理中的错误(连同示例)。大多数例子都是用Java编写的,但是它们也适用于所有其他的脚本。截至2006年12月31日,它们可以根据mapserver head的最新cvs代码进行复制。

对象相等/标识

考虑下面的JavaMaScript代码:

mapObj map=new mapObj("my.map");
layerObj layer=new layerObj(null);
// add layer to the map
int index=map.insertLayer(layer, -1);
// set its name
layer.setName("Change me");

// fetch from map
layerObj newLayer=map.getLayer(index);
// they should be the same...
System.out.println(newLayer.getName()+"=="+layer.getName());
// and this should print true (it is a reference comparison)
System.out.println(newLayer==layer);

执行时将产生以下输出:

null==Change me
false

这是因为当前的实现策略在将层插入到映射中时复制该层。Java引用没有重新指向新副本,因此与实际内存区域“断开”。这种情况目前发生在最常用的插入方法(即 插入类) .

早期垃圾收集

通过mapscript创建的对象可以“早期”被垃圾收集,当有活动对象仍在引用它们时。在Java中看到这个例子:

mapObj map=new mapObj("data/emptymap.map");

layerObj layer=new layerObj(map);
layer.setName("Layer 0");
classObj clazz=new classObj(null);
clazz.setName("Clazz 0 NULL");
int pos=layer.insertClass(clazz, -1);

map=null;
layer=null;

// force garbage collection
for(int i=0;i<100;i++)
        System.gc();

clazz.getLayer().getMap().draw();
// Java crashes because memory has been freed!

及其Perl等价物:

use mapscript;

$map = new mapscript::mapObj("../../tests/test.map");
$layer = new mapscript::layerObj($map);

print "Before first draw for $layer->{map}\n";
$layer->{map}->draw();
print "Map drawn, now undef map\n";
$map = undef;
$map1=$layer->{map}->draw();
// perl interpreter segfault

动态填充的层、类等

请参阅以下错误报告:

https://github.com/MapServer/MapServer/issues/1400

https://github.com/MapServer/MapServer/issues/1743

https://github.com/MapServer/MapServer/issues/1841

请注意,这一问题可能很难复制,信贷去塔玛斯指出它。

拟议实施

为解决第2.1、2.2和2.3项所示的问题,本招标文件提出:

  1. 引用计数器被添加到每个mapserver数据结构中,这些数据结构可以直接用mapscript操作,并插入/添加到另一个对象(即layerobj)中。

  2. 每当mapscript创建对mapserver数据结构的引用时,setter、getter和insert方法都会增加计数器的值。

  3. 所有mapscript对象始终归swig所有,或者更确切地说,归wrapper对象所有(*swigcmown*始终为true)

  4. 修改mapserver free*方法,以便只有当计数器为零时才释放底层数据结构,否则减少它。

  5. 对包装器对象进行扩充,以仅维护对其父对象的引用,并防止早期垃圾收集

  6. 所有结构数组(map->layers等)都将更改为指针数组,以消除对insert方法执行复制操作的需要。

通过初步讨论,决定放弃完全支持对象平等的要求。因此,将实施2.1,以便 only 第一个比较返回true。

此RFC应适用于所有mapscript语言(经过必要的修改)。Perl或Java的例子是因为作者熟悉这些语言。

以下小节将更详细地描述上述各项。第3.4和3.5小节为layerObj类提供了一个实现示例。请注意,在以下内容中,我们将分析的范围限制为类/层关系。

实现引用计数器

实现此RFC的mapscript对象将得到一个新的 int 成员称为 再计数.

mapscript将保持对引用计数器的只读访问,这对调试很有用

参考计数器的递增和递减将由以下宏实现:

#define MS_REF_INCR(obj) obj->refcount++
#define MS_REF_DECR(obj) (--(obj->refcount))

另一种方法是在全局哈希图中保持引用计数,该哈希图由内存地址键控。这将消除对 every 对象,但可能对性能产生更大的影响。尤其是哈希函数必须仔细选择。

http://en.wikipedia.org/wiki/Hash_table

跟踪器上提出的示例实现 bug #2032 采用第一种策略。

3.1.1 CGI中的重新计数

应修改RFC,以建议将使用mapscript要求删除在以下动机上:

  1. 随着使用mapscript的出现,如果用户需要构建mapscript而不是CGI,那么编译过程将不同。虽然这对个人来说不算什么,但可能会给那些维护诸如ms4w或fwtools之类的二进制发行版带来额外的负担。

  2. 启用refcounting不会损害CGI,因为额外的if和++

  3. 以后我们可以选择引入use_mapscript

  4. 它简化了构建过程,保持到目前为止的状态

  5. 它简化了开发人员的生活,因为少了一个定义来维护

以下是记录使用“MapScript”的文本(现在已过时)。

由于CGI不会使用此成员,因此,RFC建议:

  1. 它由一个新的define use_mapscript包装

  2. 一个新的 configure 添加了名为*--启用mapscript的选项*

  3. 从5.0开始,需要指定*--启用mapscript*来构建任何mapscript(可能除了php)

例子:

/* CLASS OBJECT - basic symbolization and classification information */
typedef struct class_obj{
#ifndef USE_MAPSCRIPT
        #ifdef SWIG
        %immutable;
        #endif /* SWIG */
        int refcount;
        #ifdef SWIG
        %mutable;
        #endif /* SWIG */
#endif
        expressionObj expression; /* the expression to be matched */
#endif

选票计数:

+1:Umberto、Tamas和Daniel

添加对mapscript包装对象的引用

将修改mapscript对象,使其保持对添加到其中的其他mapscript对象的引用,就像C结构已经做的那样。此对象在下文中称为 父对象.

例如,对于layerObj,layerObj类将被扩展为包含

  • 对包含层的mapobj的引用

这些更改的目的是宿主语言了解这些对象之间的关系,并解决早期垃圾收集问题。当层取消引用其父对象(grep'layer->map'.c*报告105种用法)时,这对于避免宿主语言中的意外崩溃也很重要。

如前所述,已决定放弃完全支持对象平等/标识的要求。

这个项目将在第二个阶段中实现,在基本的refcounting到位之后。此外,RFC建议对使用父映射引用的层操作执行父级非空检查。

更改指针数组中的结构数组

这一变化将发生在C级,是一个相当大的任务。最初,为了这个RFC的目的,数组的大小仍然是固定的,就像现在一样。对代码的修改将成为未来的RFC寻址动态大小数组的基础。

策略如下:

  1. 对数组元素的所有访问都可以用一个方便的C宏进行包装。这样,实现就从客户机代码中抽象出来了。

  2. 将修改结构定义和free/init方法以实现新功能

  3. 将修改宏以适应上一项的实现

要实现项目1,我们将使用如下Perl饼图:

perl -pi -e "s/([mM])ap->layers\[(.*?)\]\]\./GET_LAYER(\1ap, \2\])->/g" *.c
perl -pi -e "s/([mM])ap->layers\[(.*?)\]\./GET_LAYER(\1ap, \2)->/g" *.c
perl -pi -e "s/([mM])ap->layers\[(.*?)\]\]/GET_LAYER(\1ap, \2\])/g" *.c
perl -pi -e "s/([mM])ap->layers\[(.*?)\]/GET_LAYER(\1ap, \2)/g" *.c
perl -pi -e "s/dst->layers\[(.*?)\]\./GET_LAYER(dst, \1)->/g" *.c
perl -pi -e "s/src->layers\[(.*?)\]\./GET_LAYER(src, \1)->/g" *.c
perl -pi -e "s/dst->layers\[(.*?)\]/GET_LAYER(dst, \1)/g" *.c
perl -pi -e "s/src->layers\[(.*?)\]/GET_LAYER(src, \1)/g" *.c

这是将要使用的宏:

#define GET_LAYER(map, pos) map->layers[pos]

这将只留下很少的事件(约4或5),必须手工编辑。同样的方法也将用于其他结构数组(类和样式)。

此项已在不使用get_class宏的情况下为类实现。

保持父引用和C内部结构同步

为了使上一项中描述的机制能够工作,必须修改mapscript对象和本机代码中的某些函数,以便mapscript对象和C数据结构保持同步。

回到layerobj的例子 插入类, getClassremoveClass 必须修改方法以使父引用与C数据结构保持同步。当前方法将通过使用特定的类型映射进行修改。还需要修改构造函数来存储对mapobj的引用。最后,实际执行复制和插入操作的本机代码必须修改为只执行插入操作(layerObject.c,第52行,函数 MnSerSert类) .

mapscript API将向后兼容。

析构函数服从参考计数器

在释放内存之前,各种free*方法必须检查计数器。这将在本机代码中实现,如下例所示:

void msFreeMap(mapObj *map) {
        if(!map) return;
        if ( MS_REF_DECR(obj) > 0) return;
        // go on destroying the object as usual
}

这将确保在父级之前被垃圾收集的情况下不会释放子级。为了避免家长试图双倍释放部分子女:

  1. 如果父级在其子级之前被销毁,则父级应使子级中的指针无效。

  2. Viceversa,当孩子比他们的父母更早被接收时,他们必须使他们在父母中的指针无效(我们正在评估一旦父母引用到位是否会发生这种情况)

始终将对象所有权授予Swig

要使引用计数起作用,所有对象所有权都必须授予swig。这和今天的情况大不相同。但是,更改很简单,因为Swig在默认情况下会获得对象所有权,并且只需要删除Swig接口文件中的所有*%newObject*语句。

目前有58个*%newObject*语句。

C还必须在 CSMask.:

csharp/csmodule.i:375:  if (map != null) this.swigCMemOwn = false;$excode
csharp/csmodule.i:379:  if (layer != null) this.swigCMemOwn = false;$excode
csharp/csmodule.i:383:  if (parent_class != null) this.swigCMemOwn = false;$excode

或者完全删除构造函数自定义。

Java:LabelObjo的MpScript代码示例

Tamas提出了一种更面向对象的方法来解决这个问题,它可以被那些支持面向对象的语言所采用。

layerobj(javamodule.i)的代码示例:

/*
   Modified constructor according to:
   - cache population and sync, item 3.2
*/
%typemap(javaconstruct) layerObj(mapObj map) %{ {
        this($imcall, true);
        if (map != null) {
                /* Store parent reference, item 3.2 */
                this.map=map;
        }
}
%}

%typemap(javaout) int insertClass {
        // call the C API, which needs to be modified
        // so that the classObj is not copied anymore
        int actualIndex=$jnicall;
        /* Store parent reference, item 3.2 */
        classobj.layer=this;
        return actualIndex;
}

%typemap(javacode) layerObj %{
        /* parent reference, item 3.2 */
        mapObj map=null;
%}

%typemap(javacode) classObj %{
        /* parent reference, item 3.2 */
        layerObj layer=null;
%}

用于layerobj的perl:mapscript代码示例

layerObj(plmodule.i)的代码示例:::

%feature("shadow") layerObj(mapObj *map)
%{
sub new {
        my $pkg = shift;
        my $self = mapscriptc::new_layerObj(@_);
        bless $self, $pkg if defined($self);
        if (defined($_[0])) {
                # parent reference
                mapscript::LAYER_ADD_MAP_REF($self, $_[0]);
        }
        return $self;
}
%}

%feature("shadow") ~layerObj()
%{
sub DESTROY {
        return unless $_[0]->isa('HASH');
        my $self = tied(%{$_[0]});
        return unless defined $self;
        delete $ITERATORS{$self};
        mapscriptc::delete_layerObj($self);
        # remove parent reference
        delete $mapscript::LAYERMAP{$self};
}
%}

%perlcode %{
        %LAYERMAP={};
        sub LAYER_ADD_MAP_REF {
                my ($layer, $map)=@_;
                #print "MAP key=" . tied(%$layer) . "\n";
                $LAYERMAP{ tied(%$layer) }=$map;
        }

        ##################
        # DEBUGGING ONLY #
        ##################
        sub getLayerFrom {
                my ($map, $idx)=@_;
                return $MAPLAYERS{tied(%$map)}->{$idx};
        }

        sub getMAPLAYERS {
                return \%MAPLAYERS;
        }
%}

实施计划

对于大多数MaScript(Java、CpHARP、Perl和Python),在SWIG中有足够的功能来实现这个RFC中描述的特性。对于Ruby,我们可能需要走一条不同的路线,实现*%trackobjects*特性来实现3.2。至于TCL,我目前不知道是否有可能。

下面的两个部分详细描述了所需的swig-mapscript特性(代码注入和构造函数定制)。每种语言都会得到一个特定的部分来处理其自身的特性

正在检查swig mapscript功能:%javacode

swig为以下语言提供了相当于*%javacode*:

  1. Perl到%PerlCode

  2. python通过%pythoncode

  3. CSharp通过%CScode

  4. Ruby的包装对象中根本没有任何Ruby代码

  5. swig tcl不支持%tclcode构造

这个swig构造将用于在包装器中注入3.2中描述的引用的定义和包装器方法。

检查swig mapscript功能:构造函数自定义

用于包装和自定义MaScript对象的结构体(项目3.3)的%CSCuto只能在CSHARP和Java中使用。应该可以在python中使用*%pythonprepend*或*%pythonprepend*模拟其行为,在perl中使用*%perlcode*或*%feature(“shadow”)*模拟其行为。

此swig构造将用于填充父级backreference。

Java与C锐

SWIG Java和SWIG CSARP共享一个共同的基础,因此非常相似。SWIG Java构造的名称可以通过改变 java 前缀为 cs (即 javacode 在里面 CSC码, javaconstruct 在里面 csconstructjavaout 在里面 出库) .

实施应完全遵循本RCF,或基于Tamas在最后一个讨论主题中提出的建议。

备注

截至2008年1月,Tamas决定自己编写 MS RFC 24:MapScript内存管理 对C

珀尔

在上面的示例中,大多数Perl定制都可以使用 shadow 构造。

实现将完全遵循这个RFC。

Python

python在swig中享有一级支持,因此RFC应该按照描述的方式实现。

红宝石

需要调查,我们可能需要使用rb_gc_ux函数来标记对象并防止其垃圾收集,或者使用*%trackobjects*。从3.2项开始,Ruby将不实现这个RFC。

Tcl

需要调查和TCL专家。在编写本文时,TCL可能不会在第3.2项中实现此RFC。

PHP

php-mapscript不依赖swig,但是由于大多数代码是本机代码,所以应该可以采用这个rfc。

实施检查表

下表将用于跟踪此RFC的实现状态。每个mapscript对象都有一个表,当一种语言为一个给定的对象实现了这个rfc时,维护人员将用以下标记之一填充相对单元格:

  • <if the child->parent reference has been implemented

  • 如果对象的引用计数器已增加(在cnt=counter列下),则为加号(+)

  • 如果对象的引用计数器已减少(在cnt=counter列下),则为减号(-)

  • =1,如果参考计数器已重置为1(在cnt=counter列下)

MAPbOBJ

方法

CNT

爪哇

C#

珀尔

Python

tcl

ruby

mapObj(构造器)

+

GET符号集

获取字体集

图形标签缓存

获得范围

setSaved_extent

getSaved_extent

地球生物色

获取输出格式

获取引用

获取标尺

盖特传奇

获取查询文件

获取Web

获取配置

插入层

+

<

<

<

遥控器

-

获得层

+

<

<

<

GETLay-BelyNAME

+

<

<

<

准备图像

设置输出格式

图纸查询

拉锯传奇

牵引标尺

图纸参考图

获取标签

下标签

按名称获取输出格式

追加输出格式

删除输出格式

克隆

= 1

层对象

方法

CNT

爪哇

C#

珀尔

Python

tcl

ruby

layerobj(构造函数)

+

<

<

<

获得地图

获取网站

获取元数据

克隆层

= 1

插入类

+

<

<

<

卸货舱

-

下一个形状

获取特征

获取形状

获得结果

获取类

+

<

<

<

获得结果

附加特征

获得范围

类对象

方法

CNT

爪哇

C#

珀尔

Python

tcl

ruby

Classobj(构造函数)

+

<

<

<

获取标签

获取元数据

获得层

克隆

= 1

创建Legendicon

拖拉图标

获取样式

+

插入样式

+

重塑风格

-

韦博伊

方法

爪哇

C#

珀尔

Python

tcl

ruby

Webobj(构造函数)

获得地图

获得范围

设置范围

获取元数据

样式对象

对于StyleObj,当从容器对象中提取它们时,就足以拒绝它们。不需要添加指向容器对象的引用。

方法

CNT

爪哇

C#

珀尔

Python

tcl

ruby

StyleObj(构造函数)

+

设置颜色

获取颜色

获取背景颜色

后退背景颜色

获取输出颜色

设置输出颜色

获得颜色

设置最小颜色

获得最大颜色

设置最大颜色

克隆

= 1

拉贝洛夫

对于labelobjs来说,当它们从容器对象中提取时,就足以拒绝它们。不需要添加指向容器对象的引用。

方法

爪哇

C#

珀尔

Python

tcl

ruby

Labelobj(构造函数)

获取颜色

设置颜色

设置输出颜色

获取输出颜色

设置阴影颜色

获得阴影颜色

获取背景颜色

后退背景颜色

后退地面阴影颜色

获取背景阴影颜色

哈希表对象

对于hashtableobj,当从容器对象(即layerobj)中提取它们时,就足以使它们脱离。不需要添加指向容器对象的引用。

方法

爪哇

C#

珀尔

Python

tcl

ruby

hashTableObj(构造函数)

色度Objo

对于colorobj,当从容器对象中提取它们时,就足以使它们不受支持。不需要添加指向容器对象的引用。

方法

爪哇

C#

珀尔

Python

tcl

ruby

colorobj(构造函数)

图像对象

对于ImageObjs,当从容器对象中提取它们时,足够拥有它们。不需要添加指向容器对象的引用。

方法

爪哇

C#

珀尔

Python

tcl

ruby

ImageObj(构造函数)

形状对象

对于shapeobjs,当它们从容器对象中提取或添加时,足够正确地设置所有权。不需要添加指向容器对象的引用。

方法

爪哇

C#

珀尔

Python

tcl

ruby

shapeobj(构造函数)

盖特林

得界

得到

添加

克隆

复制

缓冲区

凸壳

边界

获得形心

联合

交叉

差异

共鸣

线路对象

对于LineObjs,当它们从容器对象中提取或添加时,足够正确地设置所有权。不需要添加指向容器对象的引用。

方法

爪哇

C#

珀尔

Python

tcl

ruby

LineObj(构造函数)

获得点

得到

添加

设置

彭波托基

对于pointobjs,当它们从容器对象中提取或添加时,足够正确地设置所有权。不需要添加指向容器对象的引用。

方法

爪哇

C#

珀尔

Python

tcl

ruby

PointObj(构造函数)

托哈佩

符号集对象

对于symbolsetobjs,不需要添加指向地图的引用。

方法

CNT

爪哇

C#

珀尔

Python

tcl

ruby

符号etobj(构造函数)

设置符号(符号obj值)

获取符号

+

获取符号(int i)

+

获得符号符号

+

指数

附录符号

+

删除文件

-

符号对象

对于Symbolobjs,当它们从容器对象中提取或添加时,就足以正确设置所有权。不需要添加指向容器对象的引用。

方法

CNT

爪哇

C#

珀尔

Python

tcl

ruby

符号obj(构造函数)

+

设定点

获取点

地理信息

缝合术

公开问题

在采用/实施本RFC后,应讨论以下问题。

对象的多个所有者

这是添加到多个地图的图层的情况。这应该被禁止,因为层只有一个父引用。在插入时,代码应该检查c父引用是否不为空,在这种情况下引发 errorObj 将由宿主语言在异常情况下转换。

解决方法:用户应该克隆对象,并将克隆添加到第二个映射中。

API兼容性

通过在方法签名和用法(即调用顺序、类型、返回代码等)方面保持API向后兼容,保留MapScript用户所做的投资是本RFC的首要任务。

如果该规则有任何例外,则必须证明其合理性,并根据本节进行说明。

状态

RFC于2007年1月10日在mapserver-dev上发表了一篇博文,发表了评论。

在对mapserver-dev进行讨论后正在进行修订的RFC。

在MapServer Web上发布了新修订。

2007年4月4日投票结束时通过的RFC:

+1:Umberto、Pericles S.Nacionales、Howard Butler、Stephen Woodbridge

+0:Frank Warmerdam先生

活动

这个 bug #2032 将用于跟踪与此RFC相关的活动。

2007年2月20日:附加补丁,用于转换动态分配指针数组中的map->layers(项目3.3)

2007年2月24日:关于MapServer开发的投票提议

2007年4月4日:通过RFC,正在实施中

2008年10月1日:完成RFC实施