El código Delphi Win64 puede fallar donde el mismo fuente corre limpio en Win32, y el componente Delphi PDF de HotPDF se topó con cinco casos así durante una pasada reciente de endurecimiento: Power(10, N) bindeando al overload Single, un bucle while leyendo un TList.Count caducado, una cota High(Int64) que redondea hacia arriba hasta 2^63, texto float de 15 dígitos en FPC, y asserts de test que dejan de compilar
Ninguno aparece si usted solo compila y testea Win32, que es justo como se colaron. Los casos de abajo vienen de los importadores SVG y XPS de HotPDF, de su renderer de páginas y de su lector de jobs JSON, y los resultados numéricos citados se reprodujeron con pequeños programas sonda compilados para Win32 y Win64. Si está moviendo una base de código Delphi a 64 bits, cada uno merece un grep
¿Por qué Power(10, 100) desborda solo en Win64?
En Win64, System.Math.Power(10, N) con argumentos enteros se resuelve al overload Single, así que el resultado se calcula y devuelve en precisión simple y todo lo que pase de unos 3.4E38 desborda. En Win32 la misma llamada bindea al overload Extended y corre sobre la FPU x87 con precisión de 80 bits, así que Power(10, 100) es sencillamente 1E100
System.Math declara Power para Extended, Double y Single, más una familia IntPower a juego que Power llama cuando el exponente es un número entero. En Win64, Extended es solo un alias de Double (SizeOf(Extended) = 8), y para dos argumentos enteros el compilador escoge la versión Single. El delator es la precisión, no solo el desborde: en Win64, Power(10, 20) devuelve 1.0000000200408773E20, que es exactamente Single(1E20). Un resultado Double imprimiría 1E20. Vimos el mismo bindeo con cada compilador Win64 que probamos, de Delphi 10.3 a la versión de compilador 37.0
Lo que pasa después depende de la máscara de excepciones de coma flotante. Delphi 12 y posteriores enmascaran por defecto todas las excepciones FP, así que el desborde es silencioso: Power(10, 100) devuelve +Inf y Power(10, -100) devuelve 0. Delphi 11 y anteriores dejan exOverflow sin enmascarar, y la misma llamada lanza EOverflow. Las aplicaciones que fijan la máscara por su cuenta, y las DLL cargadas en tales hosts, obtienen el comportamiento que el host escoja, que es por lo que una biblioteca no puede asumir ningún desenlace
uses
System.SysUtils, System.Math;
procedure ShowPowerOverload;
var
N: Integer;
OldMask: TArithmeticExceptionMask;
begin
N := 20;
// Win32 imprime 1E20; Win64 imprime 1.0000000200408773E20 (overload Single)
Writeln(FloatToStrF(Power(10, N), ffGeneral, 17, 0));
// Reproduce lo que hace Delphi 11, o un host con ajustes FP estrictos
OldMask := GetExceptionMask;
SetExceptionMask(OldMask - [exOverflow, exInvalidOp]);
try
N := 100;
Writeln(Power(10, N)); // Win64: EOverflow; Win32: 1E100
finally
SetExceptionMask(OldMask);
end;
end;
Desenmascarar exOverflow y exInvalidOp durante un test es la manera más barata de ver lo que ve un compilador más viejo o un host estricto. En un compilador moderno con ajustes por defecto el bug no casca, produce infinitos y ceros, y esos son mucho más difíciles de avistar en un log de tests. Restaure la máscara previa en finally: la máscara es estado por hilo, y el resto de la pasada de tests hereda lo que usted deje ahí
Cómo el overload llegó a la importación SVG y XPS de HotPDF
Los lectores de paths SVG y XPS de HotPDF comparten un único escáner de números, y ese escáner escalaba la mantisa con Power(10, Exponent) en cuanto había leído un exponente. Cualquier SVG pasado a THotPDF.ImportSVGFormXObject (el punto de entrada tras importar SVG a PDF como form XObjects reutilizables), y cualquier geometría de path manejada durante la conversión de XPS y OpenXPS a PDF, podía alimentar a esa llamada con una coordenada como 1e100 o 5e99
v2.770.91 ya topaba el exponente en 100 y rechazaba valores que pasarían de 1E300, lo que parecía suficiente: 1E100 está muy lejos del límite Double de unos 1.8E308. En Win64 seguía desbordando, porque el cálculo nunca ocurrió en Double. Desde v2.770.155 el escáner construye la potencia de diez él mismo, y números como 1e-100, o una mantisa larga con un exponente negativo grande, se leen como su valor real en lugar de colapsar a 0
Una potencia de diez segura para exponentes acotados
Cuando el exponente está acotado, la potencia de diez más segura es la que usted construye con multiplicaciones Double. Un bucle de como mucho 100 multiplicaciones no cuesta nada al lado de escanear el texto alrededor, jamás produce un intermedio mayor que la escala final, y se comporta idéntico en Win32, Win64 y Free Pascal
const
MaxDecimalExponent = 100;
function TryScaleByPowerOf10(const Value: Double; Exponent: Integer;
out Scaled: Double): Boolean;
var
Scale: Double;
I: Integer;
begin
Scaled := 0;
Result := False;
if (Exponent < -MaxDecimalExponent) or (Exponent > MaxDecimalExponent) then
Exit;
// Rechace resultados que saldrían del rango Double
if (Exponent > 0) and (Value <> 0) and
(Log10(Abs(Value)) + Exponent > 300) then
Exit;
Scale := 1.0;
for I := 1 to Abs(Exponent) do
Scale := Scale * 10.0; // jamás supera 1E100
if Exponent >= 0 then
Scaled := Value * Scale
else
Scaled := Value / Scale; // divida: 1E-100 no tiene Double exacto
Result := True;
end;
Tres detalles cargan con el peso. El check de rango usa dos comparaciones en lugar de Abs(Exponent) <= 100, porque Abs(Low(Integer)) sigue siendo negativo y colaría directo. Los exponentes negativos dividen por la escala en lugar de multiplicar por un 1E-100 precalculado, que no tiene Double exacto y añadiría un paso de redondeo más. Y la pre-comprobación con Log10 rechaza resultados fuera del rango Double antes de que la multiplicación tenga ocasión de desbordar
Sea claro con lo que el bucle sacrifica. Las potencias de diez hasta 1E22 son exactas en Double; más allá cada multiplicación redondea, y tras 100 de ellas la escala queda a unas pocas unidades del último lugar de la 1E100 redondeada correctamente. Para coordenadas de dibujo eso es invisible. Para una conversión general de texto a double que deba reproducir cada valor bit a bit no basta, y necesita un algoritmo de conversión correctamente redondeado en su lugar
Cuando dcc64 lee un TList.Count caducado en un bucle while
Observamos al compilador Win64 (dcc64, versión de compilador 37.0) generar código para un bucle while List.Count > Start do que borraba desde el final de la lista y comparaba contra un temporal de pila en lugar de releer Count. La reescritura que lo arregló fue un bucle for ... downto, cuyas cotas se evalúan exactamente una vez por definición
El bucle llegó en v2.769.3, que enseñó al código de grupos de transparencia del renderer a mantener vivas a través de un render de dos pasadas las soft masks creadas dentro de un grupo y liberarlas después. La limpieza se sentaba en un bloque finally tras un bucle for de una o dos pasadas, dentro del bucle por tile. Reducido a su forma, el antes y el después quedan así:
// La forma que vimos malcompilada por dcc64 (versión de compilador 37.0)
procedure DropMasksWhile(Masks: TList; Start: Integer);
begin
while Masks.Count > Start do
begin
TObject(Masks[Masks.Count - 1]).Free;
Masks.Delete(Masks.Count - 1);
end;
end;
// Sustituto: las cotas se evalúan una vez, no hay temporal que caduque
procedure DropMasksFrom(Masks: TList; Start: Integer);
var
Idx: NativeInt; // TList.Count es NativeInt desde Delphi 12
begin
for Idx := Masks.Count - 1 downto Start do
begin
TObject(Masks[Idx]).Free;
Masks.Delete(Idx);
end;
end;
En el código Win64 generado, el Count de la condición del bucle y el Count leído dentro del cuerpo compartían una ranura de pila. La condición comparaba contra esa ranura al entrar, antes de que nada la hubiera escrito, y nada la refrescaba tras Delete. Cuando un grupo no había creado soft masks propias, el cuerpo corría igual y le pedía el elemento -1 a una lista vacía, así que en builds de 64 bits toda página que contuviera semejante grupo de transparencia fallaba con EListError. El código Win32 del mismo fuente era correcto, y v2.770.1 sustituyó el bucle
No lo hemos reducido a una reproducción mínima, y un bucle pequeño aislado como DropMasksWhile bien puede compilar correctamente; el try/finally circundante y los bucles anidados parecen importar. Trátelo como una generación de código que observamos en una versión de compilador, no como un defecto conocido de todo compilador Win64. La lección práctica es más barata que la causa raíz: un bucle cuya condición relee el recuento de una colección mientras el cuerpo encoge esa colección merece reescribirse como un for ... downto de cota fija, y los cambios en el renderer necesitan una pasada completa de tests Win64, no solo Win32
Localizar un crash que solo muestra un build Win64 optimizado
El fallo solo se reproducía en el build Win64 optimizado, así que la ubicación salió de herramientas fuera del IDE. Un pequeño programa sonda registraba un vectored exception handler con AddVectoredExceptionHandler, capturaba la pila en la primera excepción con RtlCaptureStackBackTrace, y traducía las direcciones de retorno a nombres de función usando el map file detallado que el linker escribe con -GD. Desensamblar esa función mostró entonces la comparación leyendo una ranura de pila, [rbp+0x298], que solo se escribía dentro del cuerpo del bucle. Ese es el nivel de evidencia que usted quiere antes de culpar a un compilador, y llevó menos tiempo que pisar un build release con el depurador
¿Por qué High(Int64) no es una cota superior segura para un Double?
Un Double no puede representar High(Int64): convertir 9223372036854775807 a Double redondea hacia arriba exactamente a 2^63, uno más que el mayor Int64. En Win64 esa conversión ocurre dentro de la propia comparación, así que D <= High(Int64) es True para D = 2^63, y el Round o Trunc que viene después desborda
Win32 lo esconde por la misma razón por la que escondía el problema de Power. La comparación corre en precisión Extended de 80 bits con mantisa de 64 bits, donde High(Int64) es exacto y 2^63 compara correctamente como mayor. Win64 no tiene un tipo más ancho al que recaer. La conversión fuera de rango tampoco es bonita: en nuestros tests Win64 Round(2^63) devolvía Low(Int64), un vuelco de signo silencioso, tuviera exInvalidOp máscara o no. Win32 devuelve el mismo valor con máscara y lanza EInvalidOp sin ella
| Expresión | Win32 | Win64 |
|---|---|---|
Power(10, N), N = 20 | 1E20 | 1.0000000200408773E20 |
Power(10, 100), excepciones enmascaradas (por defecto en Delphi 12+) | 1E100 | +Inf |
Power(10, 100), exOverflow sin máscara | 1E100 | EOverflow |
D <= High(Int64), D = 2^63 | False | True |
Round(2^63), exInvalidOp sin máscara | EInvalidOp | Low(Int64) |
HotPDF se topó con esto en el lector JSON tras los valores de job de documento. JSON no pone límite de rango a los números, y el antiguo serializador convertía cualquier valor con Frac(Value) = 0 en un entero con Round, así que un 1e19 perfectamente legal se convertía en un entero equivocado o en una excepción, según la máscara. Desde v2.770.169 un número entero se escribe como entero solo cuando cabe en Int64, todo lo demás conserva su texto de coma flotante, y los getters de enteros devuelven el valor por defecto del llamador para valores fuera de rango en lugar de uno envuelto
const
TwoPow63 = 9223372036854775808.0; // 2^63, exacto en Double y Extended
function TryDoubleToInt64(const Value: Double; out R: Int64): Boolean;
begin
R := 0;
Result := not IsNan(Value) and not IsInfinite(Value) and
(Frac(Value) = 0) and (Value >= -TwoPow63) and (Value < TwoPow63);
if Result then
R := Trunc(Value);
end;
function JsonNumberText(const Value: Double): string;
var
R: Int64;
begin
// Los llamadores rechazan NaN e infinitos primero: JSON no tiene grafía para ellos
if TryDoubleToInt64(Value, R) then
Result := IntToStr(R)
else
begin
{$IFDEF FPC}
Str(Value:24, Result); // FPC Win64 ffGeneral se detiene en 15 dígitos
Result := Trim(Result);
{$ELSE}
Result := FloatToStrF(Value, ffGeneral, 17, 0, TFormatSettings.Invariant);
{$ENDIF}
end;
end;
La cota superior es el literal 9223372036854775808.0 con un < estricto. Esa constante es 2^63, exacta tanto en Double como en Extended, así que la comparación significa lo mismo en todas las plataformas. La cota inferior puede usar >= porque -2^63 es exactamente Low(Int64). Testear IsNan e IsInfinite primero, con evaluación de cortocircuito, mantiene a NaN y los infinitos lejos de Frac y de las comparaciones, que pueden lanzar EInvalidOp cuando el host lo tiene sin máscara
¿Cuántos dígitos le da de verdad float-a-texto en Win64?
Menos de los que usted pide, en dos compiladores de tres. El FloatToStrF(Value, ffGeneral, 17, 0) de Free Pascal 3.3.1 en Win64 se detiene en 15 dígitos significativos, así que 1/3 vuelve como 0.333333333333333 y dos valores Double distintos pueden serializarse a texto idéntico. Str(Value:24, Text) seguido de Trim produce 17 dígitos significativos en notación científica, 3.3333333333333331E-001 para el mismo valor, y escribe siempre un punto como separador decimal sin importar el locale. Si HotPDF sobre FPC forma parte de su matriz de builds, las notas de soporte Win64 de HotPDF para Free Pascal y Lazarus cubren el resto de las diferencias de plataforma
Delphi acepta la petición de 17 dígitos, pero los dos objetivos Delphi siguen discrepando en la salida: FloatToStrF(0.1, ffGeneral, 17, 0) da 0.10000000000000001 en Win32 y 0.1 en Win64. La RTL Win64 puede además introducir un error de redondeo del último dígito tanto al formatear como al parsear, así que más dígitos estrechan la brecha sin garantizar que cada patrón de bits Double sobreviva a un viaje de ida y vuelta por texto. La documentación de HotPDF no hace semejante promesa, y la suya tampoco debería salvo que usted despache un formateador y un parser correctamente redondeados propios. Pase TFormatSettings.Invariant, o sustituya el separador usted mismo en versiones Delphi más viejas, para que un locale alemán o francés no escriba una coma en el JSON
¿Por qué Assert.AreEqual deja de compilar en Win64?
Assert.AreEqual(3, Length(Arr)) sobre un array dinámico compila para Win32 y falla para Win64 con E2532, "Couldn't infer generic type argument from different argument types", porque Length de un array dinámico devuelve NativeInt en Win64. Con un literal Integer a un lado y un NativeInt de 64 bits al otro, el Assert.AreEqual<T> genérico de DUnitX no logra asentarse en un único T, y la build se detiene
TList.Count dispara el mismo error desde Delphi 12, donde la propiedad pasó a ser NativeInt; Delphi 11 aún la declara como Integer. Length de un string devuelve Integer en ambas plataformas y no se ve afectado, que es por lo que el error aparece en unas units de test y no en otras. Escriba el argumento de tipo explícito, Assert.AreEqual<NativeInt>(3, Length(Arr)), y compile el proyecto de test con dcc64 antes de hacer commit. Una suite que solo compila para Win32 no le dirá que su build Win64 está rota hasta que otro la pruebe
Lista de comprobación para portar código numérico Delphi a Win64
- Busque llamadas a
Power(yIntPower(con argumentos enteros; pase valores tipadosDoubleo construya usted las potencias de diez acotadas - Corra los tests numéricos al menos una vez con
exOverflowyexInvalidOpretirados víaSetExceptionMask, en Win32 y Win64 - Escriba la cota superior
Int64como< 9223372036854775808.0, nunca<= High(Int64), y rechace NaN e infinitos antes de cualquier comparación - No convierta un número parseado a
Int64solo porqueFracsea 0; los números JSON pueden ser mucho mayores - Reescriba los bucles
whileque releenCountmientras borran elementos como buclesfor ... downtode cota fija - En FPC Win64, use
Str(Value:24, Text)cuando necesite más de 15 dígitos significativos - Use
Assert.AreEqual<NativeInt>para asserts deLengthyCount, y compile los tests con dcc64 antes de hacer commit - Tras cualquier cambio en un parser o un renderer, corra la suite de regresión completa en Win32 y Win64, no solo en uno de los dos
Los arreglos del lado de la biblioteca descritos aquí están todos en HotPDF desde v2.770.169, así que la importación SVG, la conversión XPS, el renderizado de transparencia y el manejo de jobs JSON se comportan ya igual en Win64 que en Win32. Si usted genera o procesa archivos PDF desde Delphi o C++Builder para ambas plataformas, la página del componente Delphi PDF de HotPDF tiene las descargas y la lista completa de características