A way to call C/C++ functions


#1

I am a beginner in ulisp. I read http://www.ulisp.com/show?19Q4 and tried to integrate the function of my esp32c3 Bluetooth keyboard and BLE uart into it. ulisp is very useful because many codes can be debugged in ulisp’s REPL. However, it requires implementing interfaces in C for ulisp to call, and when there are changes in the C code, it needs to be compiled and uploaded, which takes quite some time.

I couldn’t find any particularly good methods on the forum, maybe I missed something. However, I did discover a fairly good approach to invoke C/C functions without the need to modify the C/C code, thus reducing compilation time. The specific method involves defining a ulisp function called call-c-fun . When ulisp calls call-c-fun , the corresponding C++ code of call-c-fun will invoke the C/C++ function based on the ulisp parameters.

/*
 * call c function
 * (call-c-fun address [arg0 arg1 arg2])
 */
object *fn_call_c_fun (object *args, object *env);


int test_cfun(int n, char c) {
    printf("call %s %d %d\n", __FUNCTION__ , n, c);
    return 100;
}
/*
I can use 
**nm ulisp-esp.ino.elf | grep test_cfun** 
in the Arduino compilation directory to get the function address of test_cfun, and then (defvar test_cfun 1107296638)
*/
//in ulisp call c fun:
>(defvar test_cfun 1107296638)
>(call-c-fun test_cfun 32 65)
addr=1107296638 arg_len=2
get_value value=32
get_value value=65
call test_cfun 32 65
100

The code implementation can be found at: https://github.com/hyj0/ulisp-esp/tree/hyj.

To achieve more complex functionality, you have implemented call-c-fun2 , which allows parameters and return values to be objects or other types.

/*
 * call c function
 * (call-c-fun2 '(t (t nil nil...)) address arg0 arg1 ...)
 */
object *fn_call_c_fun2 (object *args, object *env)

// t indicates that the parameter is of object type, nil indicates C/C++ element type

for example:

/*
  stringlength - returns the length of a Lisp string
  Handles Lisp strings packed two characters per 16-bit word, or four characters per 32-bit word
*/
int stringlength (object *form) 

//we use nm to get stringlength's address
//in ulisp 
>(defvar stringlength-addr #x42000d08)
>(let* ((lisp-string "abcd")
       (length (call-c-fun2 '(nil (t)) stringlength-addr lisp-string)))
  (print (cons "length" length)))
("length" . 4)

Sometimes we need to change the Function pointer to execute our ulisp code. I provided a ulisp function:

(defvar g_hook_fun nil)

;return (name addr arglen)
(defun getHookFun (sign fun)
  (let* ((all (call-c-fun2 '(t ()) getAllHookFunList))
	 (lst0 (mapcan (lambda (x) (if (assoc (car x) g_hook_fun)
				     nil
				   (list x)))
		     all))
	 (lst1 (mapcan (lambda (x) (if (= 0 (length (second sign)))
				       (if (= 0 (third x))
					   (list x)
					 nil)
				     (if (= 1 (third x))
					 (list x)
				       nil)))
		       lst0)))
    (if lst1
	(let ((one (first lst1)))
	  (setq g_hook_fun (cons (list (first one) sign fun) g_hook_fun))
	  one)
      nil)))

(defun releaseHootFun (name)
  (setq g_hook_fun (mapcan (lambda (x) (if (eq name (car x))
					   nil
					 (list x)))
			   g_hook_fun)))

;getAllHookFunList is c function
```
for example i want to print ulisp object to somewhere:
```
//we call printobject

>(defvar printobject-addr #x42002376)
>(let ((call_c_fun_debug #x3fc91a24))
  (register call_c_fun_debug 0)) ;close log output
>(let* ((lisp-obj "xyz")
       (my-printc (getHookFun '(nil (nil)) (lambda (c)
					     (print "get c")
					     (print c))))
       (my-printc-addr (second my-printc))
       (my-printc-name (first my-printc)))
  (unwind-protect
      (call-c-fun2 '(nil (t nil)) printobject-addr lisp-obj my-printc-addr)
    (releaseHootFun my-printc-name)))
;output:
"get c" 
34 
"get c" 
120 
"get c" 
121 
"get c" 
122 
"get c" 
34 
34

getHookFun return (name addr arglen), “name” is passed to the “releaseHootFun” function for resource release, while “addr” is a disguised function address. When the function at “addr” is called, it invokes the defined “ulisp” function. In this example, the “printobject” function takes “lisp-obj” and our forged function as parameters, allowing us to control the output.

The code implementation can be found at my github: https://github.com/hyj0/ulisp-esp/tree/hyj.

I have used it on my ESP32-C3. Everyone is welcome to give it a try.


#2

Do you have discord? I’d like to ask you about adding custom functions to uLisp in C


#3

I don’t have a Discord account. Please discuss the relevant content here. If you want to learn how to add ulisp functions, please refer to http://www.ulisp.com/show?19Q4. I wrote this post to provide a flexible way to invoke C/C++ functions without need to add ulisp functions. Currently, it supports int, char, and object types, but does not support float and string types.


#4

Thanks @hyj0! this looks really useful.

Does it need to be a modification of ulisp, or, do you think it could itself be a ulisp extension?


#5

Oh oh oh sorry my code is messy. You are right, in fact, this function only needs to add three ulisp functions, namely call-c-fun, call-c-fun2 and getAllHookFunList (getAllHookFunList is currently a c function, and can also be changed to a ulisp function), which can be written in ulisp-extensions.ino without modifying ulisp-esp.ino. I can write a forked version to add these ulisp functions if you want


#6

Thanks, that sounds great!


#7

@hyj0 Also, I think it should be easy to to add support for passing strings in call-c-fun2, by testing stringp on the object then cstring to convert it.


#8

@hyj0
This has me pretty excited, so I updated my (very young wip) emacs ulisp-mode to make it super easy to update the references: https://gist.github.com/devcarbon-com/188a0e1ea25403122d07bf1244ea7a95

Also a wip variant of c-call-fun that can call methods on a class from lisp is included as a comment underneath all that.

All this is in the very early sketch-it-out phase, so many things like the .elf path and serial port are hard-coded in.


#9

Sorry for the late reply, the support string can be implemented by passing the address of the string to call-c-fun2, because the string is the address of the transfer in the C language. This method can support strings as input and output. For example, you can call cstring in ulisp to convert it to a c string and then use it as a parameter of call-c-fun2.
Of course, it is also feasible to convert the ulisp string to a c string in call-c-fun2, and then use it as a parameter. However, the result of returning a string is not supported, for example, the incoming string needs to be modified in the called function. And how much length to allocate as a string seems ambiguous. Do you think the conversion in call-c-fun2 is more elegant or other ideas.


#10

Oh great, your approach is elegant. I used to call the C++ function before using the nm command to get the method address and the address of the instance, and then (call-c-fun method-addr instance-address args…), because the first parameter of the C++ method is this pointer. Your solution is perhaps more elegant and general.


#11

Ah interesting, I do like the idea of being able to also return strings. So far my attempts at calling cstring from ulisp have not been successful. I’ll keep experimenting with it, but if you have a working example, I’d be very grateful.


#12

Ah, I got it working by doing this

char objstring_buffer[256];
char* obstr(object* s) {
  return cstring(s, objstring_buffer, 256);
}

// to test

void SerialPrintString (char* s) {
  Serial.println(s);
}
(defvar SerialPrintString #x10003d5d)
(defvar obstr #x1000684d)

(call-c-fun SerialPrintString (call-c-fun obstr "test"))

#13

That’s neat, I didn’t realize you could use call-c-fun like that.

It may actually be the more flexible approach, or at least, I can use your’s with obstr, but mine doesn’t work. (I don’t know what the actual runtime error is yet, as I don’t yet have an SWD debugger for it).


#14

I’m sorry to reply late, I have put the code in ulisp-extensions.Ino, you can see in https://github.com/hyj0/ulisp-esp/tree/extensions. In test.el, there are several examples of call-c-fun, including examples of calling C++ methods and c string output. I’ll post some details here as well.

; call cpp method Serial.write
(call-c-fun *Serial-write* *Serial* 97)
; note: The TestClass call I wrote will coredump and I don't know why.
;call char *cstring (object *form, char *buffer, int buflen)
;with char *buffer output
(let ((c-str-addr (call-c-fun *malloc* 32))
      (lisp-str "abcd"))
  (unless (= 0 c-str-addr)
    (unwind-protect
	(progn
	  (print (cons "c-str-addr" c-str-addr))
	  (call-c-fun *memset* c-str-addr 0 32)
	  (print (cons "pack40 empty" (call-c-fun *pack40* c-str-addr)))
	  (call-c-fun2 '(nil (t nil nil)) *cstring* lisp-str c-str-addr 32)
	  ;c'char* at c-str-addr has been change
	  (print (cons "pack40" (call-c-fun *pack40* c-str-addr)))
	  )
      (call-c-fun *free* c-str-addr))))

The code has just been ported, some details I will fix later.
in your example, (call-c-fun SerialPrintString
(call-c-fun obstr “test”)) please try to change
to (call-c-fun SerialPrintString (call-c-fun2
'(nil (t)) obstr “test”)), only call-c-fun2 support ulisp’s object, and it pass a object* to the c function obstr.
you can check out my linux ported version to debug in https://github.com/hyj0/ulisp-esp/tree/fix_bug. I can’t debug on my esp32 c3 and debug on my linux too.


PC build target?