Esto lo hemos visto todos. El usuario pulsa «Cerrar el mes», o «Emitir las facturas», o «Generar los PDF», y la aplicación se queda quieta. La ventana se pone en blanco al pasarle otra por encima, el ratón no hace nada y Windows escribe «no responde» en la barra de título. El proceso va bien, pero desde fuera no hay manera de saberlo, y el usuario acaba llamando por teléfono para preguntar si tiene que apagar el ordenador.
Pasa porque Visual FoxPro hace una cosa cada vez. Mientras recorre tus 180.000 líneas de albarán, no puede repintar la pantalla ni atender al ratón. Lo hemos rodeado durante años con DOEVENTS metido en el bucle, con un segundo .exe lanzado con RUN /N que deja un fichero de bandera cuando acaba, o pasando el trabajo a la noche con una tarea programada.
FoxCore es la librería que escribí para resolver eso de otra manera: el trabajo se lo das a otros procesos, que lo hacen por su cuenta mientras tu aplicación sigue viva. Y como son varios, se hace en menos tiempo.
Qué hace, en una frase
FoxCore arranca copias de Visual FoxPro aparte, les manda tu código tal cual está escrito, y te devuelve el resultado.
Lo importante de ese dibujo para quien lleva años escribiendo FoxPro: el código que corre ahí dentro es FoxPro del de siempre. No hay que reescribirlo en otro lenguaje, ni aprender hilos, ni montar un servicio. Es el mismo SCAN ... ENDSCAN que ya tienes, dentro de un proceso distinto.
Y para quien ha heredado un sistema en FoxPro y viene del mundo de hoy: no hay multihilo aquí, porque VFP no lo tiene. Lo que hay es paralelismo por procesos del sistema operativo, coordinados desde tu proceso principal.
Lo mínimo que hay que escribir
LOCAL loRt, lcId
loRt = CREATEOBJECT("FoxCore.Runtime")
loRt.Activate(lcMiLicencia) && la licencia, como cadena
loRt.Attach(_VFP) && engancha el motor a tu programa
loRt.MaxWorkers = 4 && cuatro procesos como mucho
lcId = loRt.RunScript("RETURN 2 + 2", "la primera")
DO WHILE !loRt.IsCompleted(lcId)
loRt.Pump() && sin esto no llega nada
ENDDO
? loRt.GetState(lcId), loRt.GetResult(lcId) && Completed 4
loRt.Shutdown()RunScript() recibe una cadena con tu código y devuelve un identificador. Cuando la llamada vuelve, la tarea ya está corriendo por ahí. El resultado es lo que tu código haga con RETURN.
Las tres cosas que hay que entender
Si de este artículo solo te quedas con un trozo, que sea este.
1. El proceso que trabaja no ve nada de lo tuyo. Nace limpio: no tiene tus variables (tampoco las PUBLIC), ni tus tablas abiertas, ni tus cursores, ni tu SET DEFAULT, ni tu sesión de datos. Abre él lo que necesite. Esto que parece una pega es lo mejor que tiene, porque una tarea que revienta no te estropea la sesión, y ocho tareas a la vez no se pisan entre ellas. Un código que funciona pegado en la ventana de comandos puede fallar como tarea, y casi siempre es por esto: ahí heredaba tu entorno.
2. Pump() es lo que deja pasar los avisos. VFP hace una cosa cada vez, también para recibir. El motor no entra en tu programa por su cuenta: se pone a la cola y espera a que le des paso, y ese paso es Pump(). Sin él no llegan los resultados ni el progreso. En un formulario, Pump() va dentro de un Timer; en un proceso de los que corren solos, dentro del bucle de espera.
3. Shutdown() cierra los procesos. Si no lo llamas, quedan procesos abiertos que nadie ve, ocupando memoria del puesto. Va al terminar, en el camino de error y en el Destroy del formulario.
Qué puedes hacer con esto en una aplicación de gestión
Lo que sigue son trabajos que encajan bien, y el motivo por el que encajan.
Facturación, recálculos, cierres. Trescientas facturas repartidas entre cuatro u ocho procesos. Cada tarea abre en SHARED las tablas que necesita, hace lo suyo y devuelve el resultado. El usuario ve la barra moverse y puede parar.
Generar documentos. PDF, Excel, informes .frx a fichero. Cada documento es independiente del anterior, así que se reparten sin pensar mucho.
Importar y exportar ficheros grandes. Un tramo por tarea. La que falle deja su error escrito y las demás siguen.
Llamadas a servicios web, a la AEAT, a un banco. Aquí el tiempo se va esperando respuesta. Esperar en otro proceso no le cuesta nada a tu pantalla, y con ocho tareas son ocho esperas a la vez.
El proceso de la noche que además quieres poder lanzar de día. El mismo código sirve para las dos cosas, porque es texto FoxPro corriente.
Cómo le pasas datos y cómo te cuenta por dónde va
Como el proceso que trabaja no ve lo tuyo, hay tres formas de darle lo que necesita, de menos a más: concatenarlo en el propio texto del código, dejárselo en una pizarra que él lee, o dejarlo en un DBF que él abre. La pizarra es la que más se usa para configuración, rutas, el ejercicio contable o el usuario que ha entrado.
* en tu aplicación
loRt.Publish("ejercicio", 2026)
* dentro del código que corre aparte
* RETURN 'ejercicio ' + TRANSFORM(runtime.GetValue('ejercicio'))Esa variable runtime la mete el motor en cada tarea, y es la única puerta de vuelta hacia ti. Con ella el código que trabaja te cuenta cómo va, escribe en tu registro de sucesos y pregunta si lo has mandado parar:
* dentro del código que corre aparte
runtime.Progress(i * 20, 'documento ' + TRANSFORM(i) + ' de 5')
runtime.Log('info', 'terminado sin novedad')
IF runtime.IsCancelled()
EXIT
ENDIFDel otro lado lo recoges preguntando (GetProgress) o registrando un objeto tuyo al que el motor llama. Y como esas llamadas te llegan dentro de tu Pump(), o sea en tu propio hilo, ese objeto puede mover una barra, escribir en un cursor o repintar sin cuidados especiales. Para parar una tarea tienes Cancel(), que espera a que el código pregunte, y Kill(), que cierra el proceso a lo bruto cuando el código no pregunta nada.
El número de factura, sin bloqueos
Este es el caso que más se pregunta, y donde se entiende el diseño. Si ocho procesos emiten facturas a la vez, ¿quién reparte el correlativo? La respuesta es que el correlativo lo sigue llevando tu aplicación, y la tarea se lo pide:
DEFINE CLASS CajaDeFolios AS Custom
nUltimo = 0
FUNCTION Siguiente(tcSerie)
THIS.nUltimo = THIS.nUltimo + 1
RETURN TRANSFORM(tcSerie) + "-" + PADL(THIS.nUltimo, 4, "0")
ENDFUNC
ENDDEFINELOCAL loCaja
* en tu aplicación, antes de lanzar las tareas
loCaja = CREATEOBJECT("CajaDeFolios")
loRt.RegisterHandler("SiguienteFolio", loCaja, "Siguiente")
* y en el código que corre aparte, donde toque numerar
* lcFolio = runtime.Call('SiguienteFolio', 'FRA')
* RETURN lcFolioNi un folio repetido y ni un FLOCK(). Esas llamadas se atienden en tu proceso, dentro de Pump(), de una en una, así que nunca hay dos a la vez y no hay nada que bloquear.
Qué hace falta en tu equipo, y qué en el del cliente
Aquí está la pregunta seria, y conviene explicar primero cómo funciona la cosa por debajo.
Windows lleva una especie de libreta donde apunta qué programa responde a cada nombre. Cuando tu código hace CREATEOBJECT("Word.Application"), Windows mira esa libreta, ve qué ejecutable sirve ese nombre y lo arranca. Apuntar algo en esa libreta es lo que llamamos registrar, y es lo que hacen los instaladores.
FoxCore usa esa libreta dos veces. Una para el motor, que es un fichero .dll que se registra al instalarlo. Y otra para los procesos que hacen el trabajo, porque también se arrancan por su nombre en la libreta. El nombre que usa de fábrica es el de Visual FoxPro 9:
loRt.WorkerProgId = "VisualFoxpro.Application.9"Eso funciona en tu equipo, donde tienes VFP instalado. En el puesto de un cliente no existe y no puede existir, porque vfp9.exe es el producto completo de Microsoft y no se puede repartir. Por eso desde la versión 3.1.0 se puede cambiar ese nombre por el de tu propio ejecutable:
loRt.WorkerProgId = "VisualFoxpro.Application.a" && VFP Advanced 10.x
loRt.WorkerProgId = "MiErp.Servidor" && tu EXE, en el puesto del clienteLa buena noticia es que casi seguro no tienes que repartir nada nuevo: puede ser el mismo .exe de tu aplicación, con una clase más dentro. Tu ejecutable ya lleva el runtime de VFP, que sí se puede repartir, y además arranca con tu entorno montado, así que hasta va más rápido que un FoxPro pelado.
Esto es lo que viaja al puesto del cliente:
| Qué | Qué hay que hacer con ello |
|---|---|
FoxCore.Runtime.dll y Nexum.dll | se copian con tu aplicación y se registra la primera; hay instalador que lo hace |
tu .exe de siempre | ya lo repartes; se registra con tu.exe /regserver |
| la licencia | va dentro de tu código, como una cadena de texto. No hay fichero que copiar |
Los dos registros piden permisos de administrador, y eso no cambia nada de lo tuyo: tu instalador ya los pide. Lo que no sirve en un puesto es registrar «solo para este usuario»: hay procesos de Windows que no ven ese registro, y entonces sale «Clase no registrada» aunque en tu equipo funcione todo.
Lo que tiene que llevar tu ejecutable para poder hacer de ayudante
Son cuatro cosas, y ninguna es larga. Las tienes todas en la plantilla que viene en el paquete.
Una clase OLEPUBLIC en tu proyecto. El nombre del fichero y el de la clase forman el nombre de la libreta: miapp.exe con la clase MiWorker da miapp.MiWorker.
Tres métodos escritos a mano en esa clase. Son los que FoxCore usa para hablarle, y hay que escribirlos porque el objeto Application de VFP no cruza hacia fuera del proceso.
El entorno montado en el Init() de esa clase. Cuando a tu .exe lo arranca Windows por la libreta, el programa principal no se ejecuta. Si montas ahí tus SET PATH, tus clases y tu conexión, el ayudante nace vacío y nadie te avisa.
Un ON ERROR en la primera línea del Init(), que no dependa de variables. Ese proceso no tiene ventana: un error sin recoger abre un diálogo que nadie puede cerrar y el proceso se queda ahí colgado. Medido contra un ejecutable real, eso son 3,465 s por tarea con el diálogo frente a 0,131 s sin él. Y el manejador tiene que apoyarse solo en funciones, porque un CLEAR ALL destruye las variables antes de que falle nada.
Así queda la clase entera, que es lo que copias en tu proyecto y compilas con él:
DEFINE CLASS MiWorker AS Custom OLEPUBLIC
*-- Aqui, y solo aqui, se monta el entorno: el programa principal
*-- del EXE no se ejecuta cuando lo arranca Windows por su nombre.
FUNCTION Init
*-- Lo primero de todo, y sin usar variables: un CLEAR ALL las
*-- destruye antes de que falle nada y el manejador reventaria.
ON ERROR STRTOFILE("ERR " + TRANSFORM(ERROR()) + ": " + MESSAGE() + ;
CHR(13) + CHR(10), ADDBS(SYS(2023)) + "miworker-errores.log", 1)
THIS.PreparaEntorno()
RETURN .T.
ENDFUNC
*-- Tu entorno: lo que tu codigo necesita para trabajar sin tu formulario.
*-- Para saber donde estas, _VFP.ServerName (SYS(16) y SYS(2004) no valen
*-- dentro de un EXE arrancado por COM).
PROCEDURE PreparaEntorno
LOCAL lcBase
lcBase = ADDBS(JUSTPATH(_VFP.ServerName))
SET TALK OFF
SET SAFETY OFF
SET EXCLUSIVE OFF
SET DELETED ON
SET DEFAULT TO (lcBase)
SET PATH TO (lcBase + "libs") ADDITIVE
* SET CLASSLIB TO (lcBase + "libs\motor.vcx") ADDITIVE
* SET PROCEDURE TO (lcBase + "libs\funciones.fxp") ADDITIVE
ENDPROC
*-- Los tres metodos con los que FoxCore le habla al ayudante.
*-- Hay que escribirlos: no se heredan.
FUNCTION DoCmd(tcComando)
IF VARTYPE(tcComando) <> "C" OR EMPTY(tcComando)
RETURN .F.
ENDIF
EXECSCRIPT(tcComando)
RETURN .T.
ENDFUNC
FUNCTION SetVar(tcNombre, tuValor)
LOCAL lcNombre
IF VARTYPE(tcNombre) <> "C" OR EMPTY(tcNombre)
RETURN .F.
ENDIF
lcNombre = ALLTRIM(tcNombre)
IF TYPE(lcNombre) = "U"
PUBLIC &lcNombre
ENDIF
STORE tuValor TO (lcNombre)
RETURN .T.
ENDFUNC
FUNCTION Eval(tcExpresion)
IF VARTYPE(tcExpresion) <> "C" OR EMPTY(tcExpresion)
RETURN .NULL.
ENDIF
RETURN EVALUATE(tcExpresion)
ENDFUNC
ENDDEFINESe compila como se compila siempre, y se registra una vez:
BUILD EXE miapp FROM miapp
miapp.exe /regserverHay una más que cuesta una tarde si no se sabe: VFP genera un identificador nuevo para la clase en cada compilación, y entonces hay que volver a registrar. Se desactiva en la casilla «Regenerate Component IDs» del diálogo de construcción, o con el quinto argumento de Project.Build().
Antes de integrar nada, hay una orden que lo comprueba arrancando tu ejecutable y ejecutando en él un código de prueba, y que te dice cuánto tarda en estar listo:
? loRt.ProbeWorker("MiErp.Servidor")Cuando arrancar cuesta más que el trabajo
Arrancar un proceso se paga. Un FoxPro pelado tarda alrededor de una décima de segundo en estar listo; un ejecutable que monta el entorno de un ERP con su conexión abierta puede irse a un segundo largo. Ese es el coste fijo de cada tarea, y de ahí sale la regla práctica: una tarea de dos décimas de segundo no compensa, porque arrancar cuesta más que el trabajo. Reparte por documento, por cliente o por tramo de tabla, no por línea.
Si tu entorno tarda en montarse, los procesos se pueden quedar para la tarea siguiente:
loRt.MaxTasksPerWorker = 25 && de fábrica es 1
loRt.WorkerInitScript = lcMontarEntorno && una vez por proceso
loRt.WorkerResetScript = "CLOSE DATABASES" && entre tarea y tareaEn la batería de pruebas de carga, 120 tareas con ocho procesos pasaron de 11,0 s y 120 arranques a 3,3 s y 11 arranques con solo cambiar esa propiedad. La casa de software que certificó esta versión en agosto de 2026, con su ERP entero montado dentro del ayudante, midió una mejora de 3,2 veces.
Lo que se paga: un proceso reutilizado arrastra lo de la tarea anterior, o sea sus PUBLIC, sus tablas abiertas y sus SET. Eso es lo que hace que el entorno siga montado, y por eso FoxCore no limpia nada por su cuenta. Lo que haya que cerrar entre tarea y tarea lo escribes tú en WorkerResetScript. Si prefieres no pensar en esto, deja la propiedad en 1 y cada tarea tendrá su proceso limpio.
Cuándo no merece la pena
Hay que decirlo igual de claro que lo demás.
- FoxCore no acelera tu código. Lo saca de en medio, que es otra cosa. Un proceso de 40 s sigue tardando 40 s; lo que cambia es que la aplicación no se queda muerta y que ocho de ellos caben en el mismo rato.
- Si la consulta va lenta por falta de un índice, seguirá yendo lenta.
- Cada proceso ocupa memoria y CPU. Cuatro u ocho en un puesto normal, no cuarenta.
- El resultado vuelve como dato sencillo: carácter, número, lógico, fecha. Un objeto de VFP no cruza bien, así que se guarda en disco y se devuelve la ruta.
Lo que no hace falta aprender
Nada de .NET, nada de hilos, nada de servicios de Windows ni de línea de órdenes. Lo que se escribe es FoxPro, y lo único nuevo son media docena de llamadas: RunScript, Pump, IsCompleted, GetResult, Cancel y Shutdown. El resto de la superficie está ahí para cuando haga falta.
Las cifras de este artículo salen de la batería de pruebas de carga y de las dos certificaciones que hizo en agosto de 2026 una casa de software con su ERP real, 47 proyectos en producción en cooperativas agrícolas: 6,1 tareas por segundo de techo con tareas mínimas, 5,97 veces más rápido con ocho procesos en tareas de 2 s, y ni un proceso huérfano al terminar.
Por dónde empezar
El paquete trae una demo que se ejecuta con DO Demo, una guía de ocho pasos en la que cada paso cabe en veinte líneas y se puede correr suelto, un recetario con cuatro casos completos (una tabla en paralelo, un formulario con barra y botón de parar, un proceso de noche y el ejecutable propio para el puesto del cliente) y doce ejemplos cortos. La instalación está en la documentación del portal y el arranque rápido, en el quickstart.
Si tu código dice CREATEOBJECT("FoxCore.FoxCoreClass"), eso es el motor anterior, que sigue existiendo y sigue soportado. Lo de este artículo es la línea 3.x, CREATEOBJECT("FoxCore.Runtime"), y la tabla de equivalencias entre los dos está al final de la referencia.