IR
irwinrodriguez.dev
Back to docs

Examples

Three complete programs, exactly as they run. They are adapted from the examples that ship in the FoxCore.Runtime 3.4.2 package, in the ejemplos\ folder: you can copy them, paste them into a .prg and run them from the Visual FoxPro IDE.

The only thing changed from the packaged ones is where the license comes from. The .prg files in the package read the JWT from a file on disk because they are loose files you run by hand and have to get it from somewhere; here it lives in a constant, which is what a real application does. See Licensing.

1. A long task that says where it is

The worker calls runtime.Progress(), and here it is collected both ways: by asking with GetProgress() and by being told through a sink. The second one is the one worth understanding.

EcoDeProgreso: a task's progress, by asking and through a sink
LOCAL loRt, loEco, lcId, lcScript, lcJwt

lcJwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...."   && paste your JWT from the customer portal here

loRt = CREATEOBJECT("FoxCore.Runtime")
IF !loRt.Activate(lcJwt)
    MESSAGEBOX("La licencia de FoxCore no es valida.", 16, "FoxCore")
    RETURN
ENDIF
loRt.Attach(_VFP)

loEco = CREATEOBJECT("EcoDeProgreso")
? "sink aceptado :", loRt.SetEventSink(loEco)

TEXT TO lcScript NOSHOW
FOR i = 1 TO 5
    runtime.Progress(i * 20, 'paso ' + TRANSFORM(i) + ' de 5')
    =INKEY(0.2)
ENDFOR
runtime.Log('info', 'terminado sin novedad')
RETURN 'listo'
ENDTEXT

lcId = loRt.RunScript(lcScript, "con progreso")
DO WHILE !loRt.IsCompleted(lcId)
    loRt.Pump()                             && the sink is called FROM HERE
ENDDO
loRt.Pump(200, 1000)                        && one last round: there may be notices queued

? "avisos recibidos :", loEco.nAvisos
? "ultimo aviso     :", loEco.cUltimo
? "ultimo log       :", loEco.cLog
? "y sin sink       :", loRt.GetProgress(lcId), loRt.GetProgressMessage(lcId)

loRt.ClearEventSink()                       && important: while it holds it, your object stays alive
loRt.Shutdown()
RETURN


DEFINE CLASS EcoDeProgreso AS Custom
    nAvisos = 0
    cUltimo = ""
    cLog    = ""

    *-- OnProgress y OnLog son OBLIGATORIOS: sin los dos, SetEventSink da .F.
    PROCEDURE OnProgress(tcTaskId, tnPercent, tcMessage)
        THIS.nAvisos = THIS.nAvisos + 1
        THIS.cUltimo = TRANSFORM(tnPercent) + "% " + TRANSFORM(tcMessage)
    ENDPROC

    PROCEDURE OnLog(tcTaskId, tcLevel, tcMessage)
        THIS.cLog = TRANSFORM(tcLevel) + ": " + TRANSFORM(tcMessage)
    ENDPROC

    *-- Este es opcional: se detecta si esta, y si no, no pasa nada.
    PROCEDURE OnTaskCompleted(tcTaskId, tcState)
    ENDPROC
ENDDEFINE
The sink is NOT called from a .NET thread: it is called by Pump(), on YOUR thread. That is why a sink method can touch the screen safely. And the Pump(200, 1000) at the end is not decoration: when the task finishes there may still be notices in the queue, and without that last round they are not delivered.

What you will see

sink aceptado : .T.
avisos recibidos :          5
ultimo aviso     : 100% paso 5 de 5
ultimo log       : info: terminado sin novedad
y sin sink       : 100,00 paso 5 de 5

2. Splitting a table across several workers

The most requested recipe, and the one most often done wrong. The key is that the cursor is NOT split: the RANGE is. Each worker opens the table on its own and handles one stretch, so nothing is shared and no locking is needed.

Summing a table in stretches across four workers
LOCAL loRt, lcDbf, i, lnDesde, lnHasta, lnPorTramo, lnTotal, lnHechas, lcTerm, lcJwt
LOCAL ARRAY laIds[4], laTerm[1]

lcJwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...."   && paste your JWT from the customer portal here

*-- Una tabla de ejemplo con 400 lineas
CREATE CURSOR ventas (id I, importe Y)
FOR i = 1 TO 400
    INSERT INTO ventas VALUES (i, i * 1.25)
ENDFOR
lcDbf = ADDBS(SYS(2023)) + "r09_ventas.dbf"
COPY TO (lcDbf)
USE IN SELECT("ventas")

loRt = CREATEOBJECT("FoxCore.Runtime")
IF !loRt.Activate(lcJwt)
    MESSAGEBOX("La licencia de FoxCore no es valida.", 16, "FoxCore")
    RETURN
ENDIF
loRt.Attach(_VFP)
loRt.MaxWorkers = 4

*-- Cuatro tramos de 100. each worker opens the table ON ITS OWN y suma el suyo.
lnPorTramo = 100
FOR i = 1 TO 4
    lnDesde = (i - 1) * lnPorTramo + 1
    lnHasta = i * lnPorTramo
    laIds[i] = loRt.RunScript( ;
        "USE '" + lcDbf + "' IN 0 SHARED ALIAS ventas" + CHR(13) + CHR(10) + ;
        "SELECT ventas" + CHR(13) + CHR(10) + ;
        "SUM importe TO lnT FOR BETWEEN(id, " + TRANSFORM(lnDesde) + ", " + TRANSFORM(lnHasta) + ")" + CHR(13) + CHR(10) + ;
        "USE IN ventas" + CHR(13) + CHR(10) + ;
        "RETURN lnT", "tramo " + TRANSFORM(i))
ENDFOR

lnHechas = 0
DO WHILE lnHechas < 4
    loRt.Pump()
    lcTerm = loRt.PollCompleted()
    IF !EMPTY(lcTerm)
        lnHechas = lnHechas + ALINES(laTerm, lcTerm, 1, ";")
    ENDIF
ENDDO

*-- and it is joined here, where there is a single thread
lnTotal = 0
FOR i = 1 TO 4
    ? "  tramo " + TRANSFORM(i) + " :", loRt.GetState(laIds[i]), loRt.GetResult(laIds[i])
    IF loRt.GetState(laIds[i]) == "Completed"
        lnTotal = lnTotal + loRt.GetResult(laIds[i])
    ENDIF
ENDFOR
? "total en paralelo :", lnTotal, "  (esperado 100250,00)"

ERASE (lcDbf)
loRt.Shutdown()
RETURN
The loop uses PollCompleted(), which returns in ONE call the ids finished since last time. With four stretches it makes no difference, but with hundreds it is the difference between one COM call per round and N. And the sum is done in the host, where there is a single thread and no problem at all.

What you will see

  tramo 1 : Completed       6312,50
  tramo 2 : Completed      18812,50
  tramo 3 : Completed      31312,50
  tramo 4 : Completed      43812,50
total en paralelo :     100250,00   (esperado 100250,00)

3. A handler of your own, called from the worker

The classic case: a sequence number. Eight processes asking for a number at the same time and not one duplicate, with an object that carries no lock at all.

CajaDeFolios: a sequence number served by the host
LOCAL loRt, loCaja, i, lnHechas, lcTerminadas, lcJwt
LOCAL ARRAY laIds[8], laTerm[1]

lcJwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...."   && paste your JWT from the customer portal here

loRt = CREATEOBJECT("FoxCore.Runtime")
IF !loRt.Activate(lcJwt)
    MESSAGEBOX("La licencia de FoxCore no es valida.", 16, "FoxCore")
    RETURN
ENDIF
loRt.Attach(_VFP)
loRt.MaxWorkers = 4

loCaja = CREATEOBJECT("CajaDeFolios")
? "handler aceptado :", loRt.RegisterHandler("SiguienteFolio", loCaja, "Siguiente")

FOR i = 1 TO 8
    laIds[i] = loRt.RunScript("RETURN runtime.Call('SiguienteFolio', 'FRA')", "folio " + TRANSFORM(i))
ENDFOR

lnHechas = 0
DO WHILE lnHechas < 8
    loRt.Pump()                             && this is where the calls get served
    lcTerminadas = loRt.PollCompleted()
    IF !EMPTY(lcTerminadas)
        lnHechas = lnHechas + ALINES(laTerm, lcTerminadas, 1, ";")
    ENDIF
ENDDO

? "folios emitidos :", loCaja.nUltimo
FOR i = 1 TO 8
    ?? " " + TRANSFORM(loRt.GetResult(laIds[i]))
ENDFOR

loRt.Shutdown()
RETURN


DEFINE CLASS CajaDeFolios AS Custom
    nUltimo = 0

    *-- not a single lock, and none needed: esto se ejecuta en el hilo del host,
    *-- de una llamada en una llamada.
    FUNCTION Siguiente(tcSerie)
        THIS.nUltimo = THIS.nUltimo + 1
    RETURN TRANSFORM(tcSerie) + "-" + PADL(THIS.nUltimo, 4, "0")
    ENDFUNC
ENDDEFINE
Because the host serves them one at a time, inside Pump(), on your thread. The serialization comes from the Pump, not from the object. runtime.Call() is synchronous: it blocks the worker until your Pump() serves it, and if you do not pump, it hits its deadline and the script gets a readable error.

What you will see

handler aceptado : .T.
folios emitidos :          8 FRA-0001 FRA-0002 FRA-0003 FRA-0004 FRA-0005 FRA-0006 FRA-0007 FRA-0008
Coming from the classic engine? These examples do not use FoxCoreClass, Run() or WaitFor(). The equivalence table is in "What no longer exists", at the end of the API Reference.

Next: API Reference ->