12 Memory Allocation
Racket uses both malloc and allocation functions provided by a garbage collector. Foreign-function and embedding/extension C code may use either allocation method, keeping in mind that pointers to garbage-collectable blocks in malloced memory are invisible (i.e., such pointers will not prevent the block from being garbage-collected).
Racket CGC uses a conservative garbage collector. This garbage collector normally only recognizes pointers to the beginning of allocated objects. Thus, a pointer into the middle of a GC-allocated string will normally not keep the string from being collected. The exception to this rule is that pointers saved on the stack or in registers may point to the middle of a collectable object. Thus, it is safe to loop over an array by incrementing a local pointer variable.
Racket 3m uses a precise garbage collector that moves objects during collection, in which case the C code must be instrumented to expose local pointer bindings to the collector, and to provide tracing procedures for (tagged) records containing pointers. This instrumentation is described further in Cooperating with 3m.
The basic collector allocation functions are:
scheme_malloc —
Allocates collectable memory that may contain pointers to collectable objects; for 3m, the memory must be an array of pointers (though not necessarily to collectable objects). The newly allocated memory is initially zeroed. scheme_malloc_atomic —
Allocates collectable memory that does not contain pointers to collectable objects. If the memory does contain pointers, they are invisible to the collector and will not prevent an object from being collected. Newly allocated atomic memory is not necessarily zeroed. Atomic memory is used for strings or other blocks of memory which do not contain pointers. Atomic memory can also be used to store intentionally-hidden pointers.
scheme_malloc_tagged —
Allocates collectable memory that contains a mixture of pointers and atomic data. With the conservative collector, this function is the same as scheme_malloc, but on 3m, the type tag stored at the start of the block is used to determine the size and shape of the object for future garbage collection (as described in Cooperating with 3m). scheme_malloc_allow_interior —
Allocates an array of pointers with special treatment by 3m: the array is never moved by the garbage collector, references are allowed into the middle of the block, and even-valued pointers to the middle of the block prevent the block from being collected. (Beware that the memory manager treats any odd-valued pointer as a fixnum, even if it refers to the middle of a block that allows interior pointers.) Use this procedure sparingly, because small, non-moving objects are handled less efficiently than movable objects by the 3m collector. This procedure is the same as scheme_malloc with the conservative collector, but in the that case, having only a pointer into the interior will not prevent the array from being collected. scheme_malloc_atomic_allow_interior —
Like scheme_malloc_allow_interior for memory that does not contain pointers. scheme_malloc_uncollectable —
Allocates uncollectable memory that may contain pointers to collectable objects. There is no way to free the memory. The newly allocated memory is initially zeroed. This function is not available in 3m.
If a Racket extension stores Racket pointers in a global or static variable, then that variable must be registered with scheme_register_extension_global; this makes the pointer visible to the garbage collector. Registered variables need not contain a collectable pointer at all times (even with 3m, but the variable must contain some pointer, possibly uncollectable, at all times). Beware that static or global variables that are not thread-specific (in the OS sense of “thread”) generally do not work with multiple places.
Registration is needed for the global and static variables of an embedding program on most platforms, and registration is needed on all platforms if the program calls scheme_main_setup or scheme_set_stack_base with a non-zero first or second (respectively) argument. Global and static variables containing collectable pointers must be registered with scheme_register_static. The MZ_REGISTER_STATIC macro takes any variable name and registers it with scheme_register_static. The scheme_register_static function can be safely called even when it’s not needed, but it must not be called multiple times for a single memory address. When using scheme_set_stack_base and when places are enabled, then scheme_register_static or MZ_REGISTER_STATIC normally should be used only after scheme_basic_env, since scheme_basic_env changes the allocation space as explained in Places and Garbage Collector Instances.
Collectable memory can be temporarily locked from collection by using the reference-counting function scheme_dont_gc_ptr. On 3m, such locking does not prevent the object from being moved.
Garbage collection can occur during any call into Racket or its allocator, on anytime that Racket has control, except during functions that are documented otherwise. The predicate and accessor macros listed in Standard Types never trigger a collection.
As described in Places and Garbage Collector Instances, different places manage allocation separately. Movable memory should not be communicated from one place to another, since the source place might move the memory before it is used in the destination place. Furthermore, allocated memory that contains pointers must not be written in a place other than the one where it is allocated, due to the place-specific implementation of a write barrier for generational garbage collection. No write barrier is used for memory that is allocated by scheme_malloc_atomic_allow_interior to contain no pointers.
12.1 Cooperating with 3m
To allow 3m’s precise collector to detect and update pointers during garbage collection, all pointer values must be registered with the collector, at least during the times that a collection may occur. The content of a word registered as a pointer must contain either NULL, a pointer to the start of a collectable object, a pointer into an object allocated by scheme_malloc_allow_interior, a pointer to an object currently allocated by another memory manager (and therefore not into a block that is currently managed by the collector), or a pointer to an odd-numbered address (e.g., a Racket fixnum).
Pointers are registered in three different ways:
Pointers in static variables should be registered with scheme_register_static or MZ_REGISTER_STATIC.
Pointers in allocated memory are registered automatically when they are in an array allocated with scheme_malloc, etc. When a pointer resides in an object allocated with scheme_malloc_tagged, etc.~the tag at the start of the object identifiers the object’s size and shape. Handling of tags is described in Tagged Objects.
Local pointers (i.e., pointers on the stack or in registers) must be registered through the MZ_GC_DECL_REG, etc. macros that are described in Local Pointers.
A pointer must never refer to the interior of an allocated object (when a garbage collection is possible), unless the object was allocated with scheme_malloc_allow_interior. For this reason, pointer arithmetic must usually be avoided, unless the variable holding the generated pointer is NULLed before a collection.
IMPORTANT: The SCHEME_SYM_VAL, SCHEME_KEYWORD_VAL, SCHEME_VEC_ELS, and SCHEME_PRIM_CLOSURE_ELS macros produce pointers into the middle of their respective objects, so the results of these macros must not be held during the time that a collection can occur. Incorrectly retaining such a pointer can lead to a crash.
12.1.1 Tagged Objects
As explained in Values and Types, the scheme_make_type function can be used to obtain a new tag for a new type of object. These new types are in relatively short supply for 3m; the maximum tag is 512, and Racket itself uses nearly 300.
After allocating a new tag in 3m (and before creating instances of the tag), a size procedure, a mark procedure, and a fixup procedure must be installed for the tag using GC_register_traversers. A type tag and its associated GC procedures apply to all places, even though specific allocated objects are confined to a particular place.
A size procedure simply takes a pointer to an object with the tag and returns its size in words (not bytes). The gcBYTES_TO_WORDS macro converts a byte count to a word count.
A mark procedure is used to trace references among objects. The procedure takes a pointer to an object, and it should apply the gcMARK macro to every pointer within the object. The mark procedure should return the same result as the size procedure.
A fixup procedure is potentially used to update references to objects that have moved, although the mark procedure may have moved objects and updated references already. The fixup procedure takes a pointer to an object, and it should apply the gcFIXUP macro to every pointer within the object. The fixup procedure should return the same result as the size procedure.
Depending on the collector’s implementation, the gcMARK and/or gcFIXUP macros may take take the address of their arguments, and the fixup procedure might not be used. For example, the collector may only use the mark procedure and not actually move the object. Or it may use mark to move objects at the same time. To dereference an object pointer during a mark or fixup procedure, use GC_resolve to convert a potentially old address to the location where the object has been moved. To dereference an object pointer during a fixup procedure, use GC_fixup_self to convert the address passed to the procedure to refer to the potentially moved object.
When allocating a tagged object in 3m, the tag must be installed
immediately after the object is allocated—
12.1.2 Local Pointers
The 3m collector needs to know the address of every local or temporary pointer within a function call at any point when a collection can be triggered. Beware that nested function calls can hide temporary pointers; for example, in
scheme_make_pair(scheme_make_pair(scheme_true, scheme_false), |
scheme_make_pair(scheme_false, scheme_true)) |
the result from one scheme_make_pair call is on the stack or in a register during the other call to scheme_make_pair; this pointer must be exposed to the garbage collection and made subject to update. Simply changing the code to
tmp = scheme_make_pair(scheme_true, scheme_false); |
scheme_make_pair(tmp, |
scheme_make_pair(scheme_false, scheme_true)) |
does not expose all pointers, since tmp must be evaluated before the second call to scheme_make_pair. In general, the above code must be converted to the form
tmp1 = scheme_make_pair(scheme_true, scheme_false); |
tmp2 = scheme_make_pair(scheme_true, scheme_false); |
scheme_make_pair(tmp1, tmp2); |
and this is converted form must be instrumented to register tmp1 and tmp2. The final result might be
{ |
Scheme_Object *tmp1 = NULL, *tmp2 = NULL, *result; |
MZ_GC_DECL_REG(2); |
|
MZ_GC_VAR_IN_REG(0, tmp1); |
MZ_GC_VAR_IN_REG(1, tmp2); |
MZ_GC_REG(); |
|
tmp1 = scheme_make_pair(scheme_true, scheme_false); |
tmp2 = scheme_make_pair(scheme_true, scheme_false); |
result = scheme_make_pair(tmp1, tmp2); |
|
MZ_GC_UNREG(); |
|
return result; |
} |
Notice that result is not registered above. The MZ_GC_UNREG macro cannot trigger a garbage collection, so the result variable is never live during a potential collection. Note also that tmp1 and tmp2 are initialized with NULL, so that they always contain a pointer whenever a collection is possible.
The MZ_GC_DECL_REG macro expands to a local-variable declaration to hold information for the garbage collector. The argument is the number of slots to provide for registration. Registering a simple pointer requires a single slot, whereas registering an array of pointers requires three slots. For example, to register a pointer tmp and an array of 10 char*s:
{ |
Scheme_Object *tmp1 = NULL; |
char *a[10]; |
int i; |
MZ_GC_DECL_REG(4); |
|
MZ_GC_ARRAY_VAR_IN_REG(0, a, 10); |
MZ_GC_VAR_IN_REG(3, tmp1); |
/* Clear a before a potential GC: */ |
for (i = 0; i < 10; i++) a[i] = NULL; |
... |
f(a); |
... |
} |
The MZ_GC_ARRAY_VAR_IN_REG macro registers a local array given a starting slot, the array variable, and an array size. The MZ_GC_VAR_IN_REG macro takes a slot and simple pointer variable. A local variable or array must not be registered multiple times.
In the above example, the first argument to MZ_GC_VAR_IN_REG is 3 because the information for a uses the first three slots. Even if a is not used after the call to f, a must be registered with the collector during the entire call to f, because f presumably uses a until it returns.
The name used for a variable need not be immediate. Structure members can be supplied as well:
{ |
struct { void *s; int v; void *t; } x = {NULL, 0, NULL}; |
MZ_GC_DECL_REG(2); |
|
MZ_GC_VAR_IN_REG(0, x.s); |
MZ_GC_VAR_IN_REG(0, x.t); |
... |
} |
In general, the only constraint on the second argument to MZ_GC_VAR_IN_REG or MZ_GC_ARRAY_VAR_IN_REG is that & must produce the relevant address, and that address must be on the stack.
Pointer information is not actually registered with the collector until the MZ_GC_REG macro is used. The MZ_GC_UNREG macro de-registers the information. Each call to MZ_GC_REG must be balanced by one call to MZ_GC_UNREG.
Pointer information need not be initialized with
MZ_GC_VAR_IN_REG and MZ_GC_ARRAY_VAR_IN_REG before
calling MZ_GC_REG, and the set of registered pointers can change
at any time—
{ |
Scheme_Object *tmp1 = NULL, *tmp2 = NULL; |
mzchar *a, *b; |
MZ_GC_DECL_REG(2); |
|
MZ_GC_VAR_IN_REG(0, tmp1); |
MZ_GC_VAR_IN_REG(1, tmp2); |
|
tmp1 = scheme_make_utf8_string("foo"); |
MZ_GC_REG(); |
tmp2 = scheme_make_utf8_string("bar"); |
tmp1 = scheme_append_char_string(tmp1, tmp2); |
|
if (SCHEME_FALSEP(tmp1)) |
scheme_signal_error("shouldn't happen!"); |
|
a = SCHEME_CHAR_VAL(tmp1); |
|
MZ_GC_VAR_IN_REG(0, a); |
|
tmp2 = scheme_make_pair(scheme_read_bignum(a, 0, 10), tmp2); |
|
MZ_GC_UNREG(); |
|
if (SCHEME_INTP(tmp2)) { |
return 0; |
} |
|
MZ_GC_REG(); |
tmp1 = scheme_make_pair(scheme_read_bignum(a, 0, 8), tmp2); |
MZ_GC_UNREG(); |
|
return tmp1; |
} |
A MZ_GC_DECL_REG can be used in a nested block to hold declarations for the block’s variables. In that case, the nested MZ_GC_DECL_REG must have its own MZ_GC_REG and MZ_GC_UNREG calls.
{ |
Scheme_Object *accum = NULL; |
MZ_GC_DECL_REG(1); |
MZ_GC_VAR_IN_REG(0, accum); |
MZ_GC_REG(); |
|
accum = scheme_make_pair(scheme_true, scheme_null); |
{ |
Scheme_Object *tmp = NULL; |
MZ_GC_DECL_REG(1); |
MZ_GC_VAR_IN_REG(0, tmp); |
MZ_GC_REG(); |
|
tmp = scheme_make_pair(scheme_true, scheme_false); |
accum = scheme_make_pair(tmp, accum); |
|
MZ_GC_UNREG(); |
} |
accum = scheme_make_pair(scheme_true, accum); |
|
MZ_GC_UNREG(); |
return accum; |
} |
Variables declared in a local block can also be registered together with variables from an enclosing block, but the local-block variable must be unregistered before it goes out of scope. The MZ_GC_NO_VAR_IN_REG macro can be used to unregister a variable or to initialize a slot as having no variable.
{ |
Scheme_Object *accum = NULL; |
MZ_GC_DECL_REG(2); |
MZ_GC_VAR_IN_REG(0, accum); |
MZ_GC_NO_VAR_IN_REG(1); |
MZ_GC_REG(); |
|
accum = scheme_make_pair(scheme_true, scheme_null); |
{ |
Scheme_Object *tmp = NULL; |
MZ_GC_VAR_IN_REG(1, tmp); |
|
tmp = scheme_make_pair(scheme_true, scheme_false); |
accum = scheme_make_pair(tmp, accum); |
|
MZ_GC_NO_VAR_IN_REG(1); |
} |
accum = scheme_make_pair(scheme_true, accum); |
|
MZ_GC_UNREG(); |
return accum; |
} |
The MZ_GC_ macros all expand to nothing when MZ_PRECISE_GC is not defined, so the macros can be placed into code to be compiled for both conservative and precise collection.
The MZ_GC_REG and MZ_GC_UNREG macros must never be used in an OS thread other than Racket’s thread.
12.1.3 Local Pointers and raco ctool --xform
When raco ctool is run with the --xform flag and a source C program, it produces a C program that is instrumented in the way described in the previous section (but with a slightly different set of macros). For each input file "name.c", the transformed output is "name.3m.c".
The --xform mode for raco ctool does not change allocation calls, nor does it generate size, mark, or fixup procedures. It merely converts the code to register local pointers.
Furthermore, the --xform mode for raco ctool does not handle all of C. It’s ability to rearrange compound expressions is particularly limited, because --xform merely converts expression text heuristically instead of parsing C. A future version of the tool will correct such problems. For now, raco ctool in --xform mode attempts to provide reasonable error messages when it is unable to convert a program, but beware that it can miss cases. To an even more limited degree, --xform can work on C++ code. Inspect the output of --xform mode to ensure that your code is correctly instrumented.
Some specific limitations:
The body of a for, while, or do loop must be surrounded with curly braces. (A conversion error is normally reported, otherwise.)
Function calls may not appear on the right-hand side of an assignment within a declaration block. (A conversion error is normally reported if such an assignment is discovered.)
Multiple function calls in ... ? ... : ... cannot be lifted. (A conversion error is normally reported, otherwise.)
In an assignment, the left-hand side must be a local or static variable, not a field selection, pointer dereference, etc. (A conversion error is normally reported, otherwise.)
The conversion assumes that all function calls use an immediate name for a function, as opposed to a compound expression as in s->f(). The function name need not be a top-level function name, but it must be bound either as an argument or local variable with the form type id; the syntax ret_type (*id)(...) is not recognized, so bind the function type to a simple name with typedef, first: typedef ret_type (*type)(...); .... type id.
Arrays and structs must be passed by address, only.
GC-triggering code must not appear in system headers.
Pointer-comparison expressions are not handled correctly when either of the compared expressions includes a function call. For example, a() == b() is not converted correctly when a and b produce pointer values.
Passing the address of a local pointer to a function works only when the pointer variable remains live after the function call.
A return; form can get converted to { stmt; return; };, which can break an if (...) return; else ... pattern.
Local instances of union types are generally not supported.
Pointer arithmetic cannot be converted away, and is instead reported as an error.
12.1.4 Guiding raco ctool --xform
The following macros can be used (with care!) to navigate --xform around code that it cannot handle:
XFORM_START_SKIP and XFORM_END_SKIP: code between these two statements is ignored by the transform tool, except to tokenize it.
Example:
int foo(int c, ...) {
int r = 0;
XFORM_START_SKIP;
{
/* va plays strange tricks that confuse xform */
va_list args;
va_start(args, c);
while (c--) {
r += va_arg(args, int);
}
}
XFORM_END_SKIP;
return r;
}
These macros can also be used at the top level, outside of any function. Since they have to be terminated by a semi-colon, however, top-level uses usually must be wrapped with #ifdef MZ_PRECISE_GC and #endif; a semi-colon by itself at the top level is not legal in C.
XFORM_SKIP_PROC: annotate a function so that its body is skipped in the same way as bracketing it with XFORM_START_SKIP and XFORM_END_SKIP.
Example:
int foo(int c, ...) XFORM_SKIP_PROC {
}
XFORM_HIDE_EXPR: a macro that takes wraps an expression to disable processing of the expression.
Example:
int foo(int c, ...) {
int r = 0;
{
/* va plays strange tricks that confuse xform */
XFORM_CAN_IGNORE va_list args; /* See below */
XFORM_HIDE_EXPR(va_start(args, c));
while (c--) {
r += XFORM_HIDE_EXPR(va_arg(args, int));
}
}
return r;
}
XFORM_CAN_IGNORE: a macro that acts like a type modifier (must appear first) to indicate that a declared variable can be treated as atomic. See above for an example.
XFORM_START_SUSPEND and XFORM_END_SUSPEND: for use at the top level (outside of any function definition), and similar to XFORM_START_SKIP and XFORM_END_SKIP in that function and class bodies are not transformed. Type and prototype information is still collected for use by later transformations, however. These forms must be terminated by a semi-colon.
XFORM_START_TRUST_ARITH and XFORM_END_TRUST_ARITH: for use at the top level (outside of any function definition) to disable warnings about pointer arithmetic. Use only when you’re absolutely certain that the garbage collector cannot be pointers offset into the middle of a collectable object. These forms must be terminated by a semi-colon.
XFORM_TRUST_PLUS: a replacement for + that does not trigger pointer-arithmetic warnings. Use with care.
XFORM_TRUST_MINUS: a replacement for - that does not trigger pointer-arithmetic warnings. Use with care.
12.1.5 Places and Garbage Collector Instances
When places are enabled, then a single process can have multiple instances of the garbage collector in the same process. Each place allocates using its own collector, and no place is allowed to hold a reference to memory that is allocated by another place. In addition, a master garbage collector instance holds values that are shared among places; different places can refer to memory that is allocated by the master garbage collector, but the master still cannot reference memory allocated by place-specific garbage collectors.
Calling scheme_main_stack_setup creates the master garbage collector, and allocation uses that collector until scheme_basic_env returns, at which point the initial place’s garbage collector is in effect. Using scheme_register_static or MZ_REGISTER_STATIC before calling scheme_basic_env registers an address that should be used to hold only values allocated before scheme_basic_env is called. More typically, scheme_register_static and MZ_REGISTER_STATIC are used only after scheme_basic_env returns. Using scheme_main_setup calls scheme_basic_env automatically, in which case there is no opportunity to use scheme_register_static or MZ_REGISTER_STATIC too early.
12.2 Memory Functions
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
In addition to global variables, this function can be used to register any permanent memory that the collector would otherwise treat as atomic. A garbage collection can occur during the registration.
|
The Scheme_Env_Main type is defined as follows:
typedef int (*Scheme_Env_Main)(Scheme_Env *env, |
int argc, char **argv); |
The result of main is the result of scheme_main_setup.
If no_auto_statics is non-zero, then static variables must be explicitly registered with the garbage collector; see Memory Allocation for more information.
|
The data argument is passed through to main, where the Scheme_Nested_Main type is defined as follows:
typedef int (*Scheme_Nested_Main)(void *data); |
|
Example:
int main(int argc, char **argv) { |
int dummy; |
scheme_set_stack_base(&dummy, 0); |
real_main(argc, argv); /* calls scheme_basic_env(), etc. */ |
} |
On 3m, the above code does not quite work, because stack_addr must be the beginning or end of a local-frame registration. Worse, in CGC or 3m, if real_main is declared static, the compiler may inline it and place variables containing collectable values deeper in the stack than dummy. To avoid these problems, use scheme_main_setup or scheme_main_stack_setup, instead.
The above code also may not work when future and/or places are enabled in Racket, because scheme_set_stack_base does not initialize Racket’s thread-local variables. Again, use scheme_main_setup or scheme_main_stack_setup to avoid the problem.
|
If stack_end is NULL, then the stack end is computed automatically: the stack size assumed to be the limit reported by getrlimit on Unix and Mac OS, or it is assumed to be the stack reservation of the executable (or 1 MB if parsing the executable fails) on Windows; if this size is greater than 8 MB, then 8 MB is assumed, instead; the size is decremented by 50000 bytes (64-bit Windows: 100000 bytes) to cover a large margin of error; finally, the size is subtracted from (for stacks that grow down) or added to (for stacks that grow up) the stack base in stack_addr or the automatically computed stack base. Note that the 50000-byte margin of error is assumed to cover the difference between the actual stack start and the reported stack base, in addition to the margin needed for detecting and handling stack overflow.
|
The tls_index argument must be 0. It is currently ignored, but a future version may use the argument to allow declaration of the thread-local variable in a dynamically linked DLL.
Changed in version 6.3: Changed from available only on 32-bit Windows to available on all Windows variants.
|
The macro MZ_REGISTER_STATIC can be used directly on a static variable. It expands to a comment if statics need not be registered, and a call to scheme_register_static (with the address of the static variable) otherwise.
|
This function is not available in 3m.
|
This function is not available in 3m.
|
The fnl_proc type is not actually defined, but it is equivalent to
typedef void (*fnl_proc)(void *p, void *data) |
The f argument is the callback function; when it is called, it
will be passed the value p and the data pointer data;
data can be anything —
To remove a registered finalizer, pass NULL for f and data.
Note: registering a callback not only keeps p from collection until the callback is invoked, but it also keeps data reachable until the callback is invoked.
|
See scheme_register_finalizer, above, for information about the arguments.
To remove an added finalizer, use scheme_subtract_finalizer.
|
See scheme_register_finalizer, above, for information about the arguments.
There is currently no facility to remove a will-like finalizer.
|
|
|
|
|
This function keeps a reference count on the pointers it registers, so two calls to scheme_dont_gc_ptr for the same p should be balanced with two calls to scheme_gc_ptr_ok.
|
|
|
When the PLTDISABLEGC environment variable is set, then racket initializes the internal counter to 1 to initially disable garbage collection.
|
Each of the three procedures takes a pointer and returns an integer:
typedef int (*Size_Proc)(void *obj); |
typedef int (*Mark_Proc)(void *obj); |
typedef int (*Fixup_Proc)(void *obj); |
If the result of the size procedure is a constant, then pass a non-zero value for is_const_size. If the mark and fixup procedures are no-ops, then pass a non-zero value for is_atomic.
|
|
|
Commands:
#define SCHEME_GC_SHAPE_TERM 0 —
the terminator command, which has no argument. #define SCHEME_GC_SHAPE_PTR_OFFSET 1 —
specifies that a object tagged with type has a pointer to be made visible to the garbage collector, where the command argument is the offset from the beginning of the object. #define SCHEME_GC_SHAPE_ADD_SIZE 2 —
specifies the allocated size of an object tagged with type, where the command argument is an amount to add to an accumulated size; currently, size information is not used, but it may be needed with future implementations of the garbage collector.
To improve forward compatibility, any other command is assumed to take a single argument and is ignored.
A GC-shape registration is place-specific, even though scheme_make_type creates a type tag that spans places. If a traversal is already installed for type in the current place, the old traversal specification is replaced. The scheme_register_type_gc_shape function keeps its own copy of the array shape, so the array need not be retained.
Added in version 6.4.0.10.
|
|