El código Delphi de Win64 puede fallar donde la misma fuente corre limpio en Win32, y el componente HotPDF Delphi PDF se topó con cinco casos así durante una pasada de endurecimiento reciente: Power(10, N) enlazando al overload de Single, un loop while leyendo un TList.Count viejo, un límite High(Int64) que redondea hacia arriba a 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 exactamente cómo se colaron. Los casos de abajo salen de los importers de 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 de prueba compilados para Win32 y Win64. Si usted 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 resuelve al overload de Single, así que el resultado se computa y devuelve en precisión simple y cualquier cosa por encima de aproximadamente 3.4E38 desborda. En Win32 la misma llamada se enlaza al overload de 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 de Single. La pista es la precisión, no solo el overflow: en Win64, Power(10, 20) devuelve 1.0000000200408773E20, que es exactamente Single(1E20). Un resultado Double imprimiría 1E20. Vimos el mismo enlace con cada compilador Win64 que probamos, desde Delphi 10.3 hasta la versión de compilador 37.0
Lo que pasa después depende de la máscara de excepciones de punto flotante. Delphi 12 y posteriores enmascaran todas las excepciones de punto flotante por defecto, así que el overflow 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 levanta EOverflow. Las aplicaciones que asignan la máscara ellas mismas, y las DLLs cargadas en tales hosts, reciben el comportamiento que el host haya escogido, razón por la cual una librería no puede asumir ninguno de los dos desenlaces
uses
System.SysUtils, System.Math;
procedure ShowPowerOverload;
var
N: Integer;
OldMask: TArithmeticExceptionMask;
begin
N := 20;
// Win32 imprime 1E20; Win64 imprime 1.0000000200408773E20 (overload de Single)
Writeln(FloatToStrF(Power(10, N), ffGeneral, 17, 0));
// Reproduzca lo que hace Delphi 11, o un host con settings 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 la duración de un test es la forma más barata de ver lo que ve un compilador más viejo o un host estricto. En un compilador moderno con settings por defecto el bug no se cuelga, produce infinitos y ceros, y esos son mucho más difíciles de distinguir en el log de un test. Restaure la máscara previa en finally: la máscara es estado por thread, y el resto de la corrida de tests hereda lo que usted deje
Cómo llegó el overload a la importación de SVG y XPS de HotPDF
Los lectores de paths de SVG y XPS de HotPDF comparten un scanner de números, y ese scanner escalaba la mantisa con Power(10, Exponent) en cuanto leía un exponente. Cualquier SVG pasado a THotPDF.ImportSVGFormXObject (el punto de entrada detrás de 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 había topeado el exponente en 100 y rechazado valores que pasaran de 1E300, lo que parecía suficiente: 1E100 está a años luz del límite de Double de cerca de 1.8E308. En Win64 igual desbordaba, porque el cómputo nunca ocurría en Double para nada. Desde v2.770.155 el scanner construye la potencia de diez por su cuenta, y números como 1e-100, o una mantisa larga con un exponente negativo grande, se leen como su valor real en vez 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 arma con multiplicaciones Double. Un loop de cuando mucho 100 multiplicaciones no cuesta nada al lado de escanear el texto que la rodea, 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 de 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 un Double exacto
Result := True;
end;
Tres detalles cargan con el peso. El chequeo de rango usa dos comparaciones en vez de Abs(Exponent) <= 100, porque Abs(Low(Integer)) sigue siendo negativa y se pasaría de largo derechito. Los exponentes negativos dividen por la escala en vez de multiplicar por un 1E-100 precomputado, que no tiene un Double exacto y sumaría un paso de redondeo más. Y el pre-chequeo de Log10 rechaza resultados fuera del rango de Double antes de que la multiplicación tenga oportunidad de desbordar
Sea claro sobre lo que el loop entrega a cambio. Las potencias de diez hasta 1E22 son exactas en Double; más allá de eso cada multiplicación redondea, y después de 100 de ellas la escala queda a unas pocas unidades del último lugar de la 1E100 correctamente redondeada. 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 alcanza, y necesita un algoritmo de conversión correctamente redondeado en su lugar
Cuando dcc64 lee un TList.Count viejo en un loop while
Observamos al compilador Win64 (dcc64, versión de compilador 37.0) generar código para un loop while List.Count > Start do que borraba desde el final de la lista y comparaba contra un temporal del stack en vez de re-leer Count. La reescritura que lo arregló fue un loop for ... downto, cuyos límites se evalúan exactamente una vez por definición
El loop llegó en v2.769.3, que le enseñó al código de transparency groups del renderer a mantener vivas las soft masks creadas dentro de un grupo a través de un render de dos pasadas y liberarlas después. La limpieza sentaba en un bloque finally después de un loop for de una o dos pasadas, dentro del loop por tile. Reducido a su forma, el antes y el después se ven así:
// La forma que vimos mal compilada 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;
// Reemplazo: los límites se evalúan una vez, no hay temporal que se quede viejo
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 loop y el Count leído dentro del cuerpo compartían un slot del stack. La condición comparaba contra ese slot al entrar, antes de que cualquier cosa lo escribiera, y nada lo refrescaba después del Delete. Cuando un grupo no había creado soft masks propias, el cuerpo corría de todos modos y le pedía el item -1 a una lista vacía, así que en builds de 64 bits cada página que contuviera tal transparency group fallaba con EListError. El código Win32 para la misma fuente era correcto, y v2.770.1 reemplazó el loop
No lo hemos reducido a una reproducción mínima, y un loop standalone pequeño como DropMasksWhile bien puede compilar correctamente; el try/finally circundante y los loops anidados parecen importar. Trátelo como 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 loop cuya condición re-lee el conteo de una colección mientras el cuerpo la achica vale la pena reescribirlo como un for ... downto de límites fijos, y los cambios al renderer necesitan una corrida completa de tests en Win64, no solo Win32
Localizar un crash que solo muestra el build Win64 optimizado
La falla solo se reproducía en el build Win64 optimizado, así que la ubicación salió de herramientas fuera del IDE. Un pequeño programa de prueba registró un vectored exception handler con AddVectoredExceptionHandler, capturó el stack en la primera excepción con RtlCaptureStackBackTrace, y tradujo 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 a la comparación leyendo un slot del stack, [rbp+0x298], que solo se escribía dentro del cuerpo del loop. Ese es el nivel de evidencia que usted quiere antes de culpar al compilador, y tomó menos tiempo que pisar el código de un build release
¿Por qué High(Int64) no es un límite superior seguro para un Double?
Un Double no puede representar High(Int64): convertir 9223372036854775807 a Double redondea hacia arriba a exactamente 2^63, uno más allá del mayor Int64. En Win64 esa conversión ocurre dentro de la comparación misma, así que D <= High(Int64) es True para D = 2^63, y el Round o Trunc que sigue desborda
Win32 lo esconde por la misma razón por la que escondió 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 correctamente compara mayor. Win64 no tiene un tipo más ancho al cual recurrir. La conversión fuera de rango tampoco es bonita: en nuestros tests de Win64 Round(2^63) devolvía Low(Int64), un volteamiento de signo silencioso, fuera exInvalidOp estuviera enmascarado o no. Win32 devuelve el mismo valor cuando está enmascarado y levanta EInvalidOp cuando no
| Expresión | Win32 | Win64 |
|---|---|---|
Power(10, N), N = 20 | 1E20 | 1.0000000200408773E20 |
Power(10, 100), excepciones enmascaradas (default de 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 detrás de los valores de jobs de documentos. JSON no pone límite de rango a los números, y el serializador viejo convertía cualquier valor con Frac(Value) = 0 en un entero con Round, así que un 1e19 perfectamente legal se volvía o un entero equivocado o una excepción, según la máscara. Desde v2.770.169 un número entero se escribe como integer solo cuando cabe en Int64, todo lo demás conserva su texto de punto flotante, y los getters de enteros devuelven el default del caller para valores fuera de rango en vez de uno enrollado
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 callers 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;
El límite 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 toda plataforma. El límite inferior puede usar >= porque -2^63 es exactamente Low(Int64). Testear IsNan y IsInfinite primero, con evaluación de corto circuito, mantiene a NaN y a los infinitos lejos de Frac y de las comparaciones, que pueden levantar EInvalidOp cuando el host lo ha desenmascarado
¿Cuántos dígitos le da de verdad el 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 siempre escribe un punto como separador decimal sin importar el locale. Si HotPDF en FPC es 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 el pedido de 17 dígitos, pero los dos targets de Delphi todavía discrepan en la salida: FloatToStrF(0.1, ffGeneral, 17, 0) da 0.10000000000000001 en Win32 y 0.1 en Win64. La RTL de Win64 también puede introducir un error de redondeo en el último dígito tanto al formatear como al parsear, así que más dígitos achican la brecha sin garantizar que cada patrón de bits de un Double sobreviva un viaje de ida y vuelta por texto. La documentación de HotPDF no hace tal promesa, y la suya tampoco debería salvo que usted despache un formateador y parser correctamente redondeados propios. Pase TFormatSettings.Invariant, o reemplace el separador usted mismo en versiones de Delphi más viejas, así un locale alemán o francés no escribe una coma dentro del JSON
¿Por qué Assert.AreEqual deja de compilar en Win64?
Assert.AreEqual(3, Length(Arr)) sobre un dynamic array compila para Win32 y falla para Win64 con E2532, "Couldn't infer generic type argument from different argument types", porque el Length de un dynamic array devuelve NativeInt en Win64. Con un literal Integer de un lado y un NativeInt de 64 bits del otro, el Assert.AreEqual<T> genérico de DUnitX no puede asentarse en un solo T, y el build se detiene
TList.Count dispara el mismo error desde Delphi 12, donde la propiedad se volvió NativeInt; Delphi 11 todavía la declara como Integer. El Length de un string devuelve Integer en ambas plataformas y no se ve afectado, razón por la cual el error aparece en algunas unidades 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 va a decir que su build Win64 está roto hasta que alguien más lo intente
Checklist de porte a Win64 para código numérico de Delphi
- Busque llamadas a
Power(yIntPower(con argumentos enteros; pase valores tipadosDoubleo arme usted mismo las potencias de diez acotadas - Corra los tests numéricos al menos una vez con
exOverflowyexInvalidOpquitados víaSetExceptionMask, en Win32 y Win64 ambos - Escriba el límite superior
Int64como< 9223372036854775808.0, jamás<= High(Int64), y rechace NaN e infinitos antes de cualquier comparación - No convierta un número parseado a
Int64solo porqueFracdé 0; los números JSON pueden ser mucho más grandes - Reescriba los loops
whileque re-leenCountmientras borran items como loopsfor ... downtode límites fijos - En FPC Win64, use
Str(Value:24, Text)cuando necesite más de 15 dígitos significativos - Use
Assert.AreEqual<NativeInt>para los asserts deLengthyCount, y compile los tests con dcc64 antes de hacer commit - Después de cualquier cambio a un parser o al renderer, corra la suite de regresión completa en Win32 y Win64, no solo en uno
Las correcciones del lado de la librería descritas aquí están todas en HotPDF desde v2.770.169, así que la importación de SVG, la conversión de XPS, el renderizado de transparencias y el manejo de jobs JSON ahora se comportan 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 HotPDF Delphi PDF tiene las descargas y la lista completa de funciones