0

In python, functions are "first class citizens" and can be passed to functions/methods as arguments.

Let's say I wanted to start writing a basic remote procedure call(rpc) library in python, I might start by creating a dictionary that maps the function names to the actual function objects:

rpc = {}  # string: function


def register_rpc(name, function):
    """ Register a function as a RPC """
    rpc[name] = function


def sum_nums(a, b):
    """ Sum numbers """
    return a + b


register_rpc("sum_nums", sum_nums) # register sum_nums as RPC

print(rpc["sum_nums"](10, 15))

I can get close to this in Nim. The issue is I have to explicitly define the arguments and argument types for the proc in the lookup Table, and this has to match the register_rpc procedure's definition as well. Here is my semi-equivalent Nim code:

import tables


var rpc = initTable[string, proc(a, b: int): int]()  # explicitly defined procedure arguments/types


# Procedure arguments/types must match Table's proc definition or else I can't register a RPC
proc register_rpc(p: proc(a, b: int): int, n: string): bool =
    #[ Register a function as a RPC ]#
    rpc[n] = p
    true


# proc definition matches Table/register_rpc
proc sum_nums(a, b: int): int =
    #[ Sum numbers ]#
    a + b


discard register_rpc(sum_nums, "sum_nums")
echo rpc["sum_nums"](10, 15)

Is there any way to create a register_rpc procedure where I don't have to explicitly define the proc arguments and their types? How can I make my Table match this as well? I asked a question, that seems semi-related, yesterday:

Can I unpack `varargs` to use as individual procedure call arguments with nim?

However I am unable to use untyped types for the Table.

Will I have to overload the register_rpc procedure to cover all my different type scenarios? How do I create a lookup table without having to explicitly define the proc arguments/types?

RattleyCooper
  • 4,997
  • 5
  • 27
  • 43

1 Answers1

3

This dynamic way of handling things is not supported in nim. Type of procs has to be known at compile time, that's just a fact. Though of course you can do something so horrible and unpleasing like:

import tables

var table = initTable[string, pointer]()

table.add("add", cast[pointer](proc(a: int, b: int): int =
  a + b
))

table.add("negate", cast[pointer](proc(a: int): int =
  -a
))

assert cast[proc(a: int, b: int): int {.cdecl.}](table["add"])(10, 20) == 30

But lets be honest here, if you accidently cast to a wrong proc, you are doomed. We need safety in this case, a way to differentiate the pointer. That's where abstraction comes in.

import macrocache, options

const counter = CacheCounter("counter")

func typeID(tp: type): int =
  const id = counter.value
  static: counter.inc
  id

{.pragma: dynProc, cdecl, noSideEffect, gcsafe, locks:0.}

type
  DynamicProc* = object
    id: int
    value: pointer

func initDynamicProc*[T](p: T): DynamicProc =
  DynamicProc(id: T.typeID, value: cast[pointer](p))

template ID*(d: DynamicProc): int = d.id

func get*(d: DynamicProc, tp: type): Option[tp] =
  if tp.typeID != d.id:
    none(tp)
  else:
    some(cast[tp](d.value))

var safeTable = initTable[string, DynamicProc]()

safeTable.add("add", initDynamicProc(proc (a, b: int): int {.dynProc.} =
  a + b
))

assert safeTable["add"].get(proc (a, b: int): int {.dynProc.}).get()(10, 20) == 30
assert safeTable["add"].get(proc (b: int) {.dynProc.}) == none(proc (b: int) {.dynProc.}) 

Its still verbose but this time you have guarantee its safe. dynProc pragma is needed to fix the type matching as type with different pragmas is considered different type.

Jakub Dóka
  • 2,477
  • 1
  • 7
  • 12