├── tests ├── srcs ├── mod │ ├── __init__.py │ └── test.py ├── modul.py ├── test4.ml ├── fetch.py ├── test5.ml ├── test8.ml ├── test1.ml ├── test7.ml ├── test6.ml ├── test3.ml └── test2.ml ├── benchmark ├── srcs ├── modul.py └── benchmark.ml ├── examples ├── srcs ├── simple.py ├── phantom.py ├── simple.ml ├── phantom.ml └── reference.ml ├── srcs ├── lymp.mldylib ├── lymp.mllib ├── META ├── LICENSE.bson.txt ├── lymp.mli ├── bson.mli ├── lymp.py ├── lymp.ml └── bson.ml ├── _oasis ├── _tags ├── Makefile ├── LICENSE ├── README.md └── myocamlbuild.ml /tests/srcs: -------------------------------------------------------------------------------- 1 | ../srcs -------------------------------------------------------------------------------- /benchmark/srcs: -------------------------------------------------------------------------------- 1 | ../srcs -------------------------------------------------------------------------------- /examples/srcs: -------------------------------------------------------------------------------- 1 | ../srcs -------------------------------------------------------------------------------- /tests/mod/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /benchmark/modul.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def get_int(): 4 | return 42 -------------------------------------------------------------------------------- /tests/mod/test.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def get_msg(): 4 | return "hi there" 5 | -------------------------------------------------------------------------------- /srcs/lymp.mldylib: -------------------------------------------------------------------------------- 1 | # OASIS_START 2 | # DO NOT EDIT (digest: 4edada10b77f0a99cbffc29e8b34715f) 3 | Lymp 4 | Bson 5 | # OASIS_STOP 6 | -------------------------------------------------------------------------------- /srcs/lymp.mllib: -------------------------------------------------------------------------------- 1 | # OASIS_START 2 | # DO NOT EDIT (digest: 4edada10b77f0a99cbffc29e8b34715f) 3 | Lymp 4 | Bson 5 | # OASIS_STOP 6 | -------------------------------------------------------------------------------- /examples/simple.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def get_message(): 4 | return u"hi there" 5 | 6 | def get_integer(): 7 | return 42 8 | 9 | def sum(a, b): 10 | return a + b 11 | -------------------------------------------------------------------------------- /tests/modul.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import bson 3 | 4 | def rand_str(): 5 | print("alright, inside python") 6 | return "salut les gars" 7 | 8 | def get_tuple(): 9 | return (1,2) 10 | 11 | def print_tuple(arg): 12 | print(arg) 13 | 14 | def first_of_tuple(arg): 15 | return arg[0] 16 | 17 | def print_arg(arg): 18 | print(arg) 19 | 20 | def ret_unicode(): 21 | return u'saluté' -------------------------------------------------------------------------------- /_oasis: -------------------------------------------------------------------------------- 1 | OASISFormat: 0.3 2 | Name: lymp 3 | Version: 0.2.2 4 | Synopsis: Use Python functions and objects from OCaml 5 | Authors: Dominik Bousquet 6 | License: MIT 7 | Plugins: META (0.3) 8 | 9 | Library lymp 10 | Path: srcs 11 | BuildTools: ocamlbuild 12 | BuildDepends: unix, threads 13 | Modules: Lymp 14 | InternalModules: Bson 15 | -------------------------------------------------------------------------------- /srcs/META: -------------------------------------------------------------------------------- 1 | # OASIS_START 2 | # DO NOT EDIT (digest: d86d9a8ff71868e64aaf64d1811c3216) 3 | version = "0.2.2" 4 | description = "Use Python functions and objects from OCaml" 5 | requires = "unix threads" 6 | archive(byte) = "lymp.cma" 7 | archive(byte, plugin) = "lymp.cma" 8 | archive(native) = "lymp.cmxa" 9 | archive(native, plugin) = "lymp.cmxs" 10 | exists_if = "lymp.cma" 11 | # OASIS_STOP 12 | 13 | -------------------------------------------------------------------------------- /examples/phantom.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import lxml.html as lx 4 | from selenium import webdriver 5 | 6 | driver = webdriver.PhantomJS() 7 | driver.set_window_size(1024, 768) 8 | 9 | def download(url): 10 | driver.get(url) 11 | driver.save_screenshot('screen.png') 12 | return driver.page_source 13 | 14 | def select(html, css_selector): 15 | doc = lx.fromstring(html) 16 | return doc.cssselect(css_selector) -------------------------------------------------------------------------------- /tests/test4.ml: -------------------------------------------------------------------------------- 1 | 2 | 3 | open Lymp 4 | 5 | let ocamlfind_ok = (try (Sys.getenv "OCAMLFIND_OK" ; true) with | _ -> false) 6 | let py = if ocamlfind_ok then Lymp.init "." else Lymp.init ~exec:"python3" ~ocamlfind:false ~lymppy_dirpath:"srcs" "." 7 | let test = Lymp.get_module py "mod.test" 8 | 9 | let () = 10 | if (get_string test "get_msg" [] <> "hi there") then raise (Failure "failed") ; 11 | 12 | close py 13 | -------------------------------------------------------------------------------- /tests/fetch.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class Fetch: 4 | def __init__(self, url, mode): 5 | self.url = url 6 | self.mode = mode 7 | 8 | def download(self): 9 | if self.mode == "phantom": 10 | return "

Hi from phantom

" 11 | return "

Hi

" 12 | 13 | def ret_self(self): 14 | return self 15 | 16 | def ret_list(self): 17 | return [1,2,self, ["salut", 3], 4] 18 | 19 | def ret_list2(self): 20 | return [1, 2] -------------------------------------------------------------------------------- /tests/test5.ml: -------------------------------------------------------------------------------- 1 | 2 | 3 | let ocamlfind_ok = (try (Sys.getenv "OCAMLFIND_OK" ; true) with | _ -> false) 4 | let py = if ocamlfind_ok then Lymp.init "." else Lymp.init ~exec:"python3" ~ocamlfind:false ~lymppy_dirpath:"srcs" "." 5 | let sys = Lymp.get_module py "sys" 6 | 7 | let () = 8 | Lymp.set_attr sys "stdin" (Lymp.Pyint 42) ; 9 | let py_stdin = Lymp.attr_int sys "stdin" in 10 | if py_stdin = 42 then () else raise (Failure "failed") ; 11 | 12 | Lymp.close py -------------------------------------------------------------------------------- /tests/test8.ml: -------------------------------------------------------------------------------- 1 | 2 | 3 | let ocamlfind_ok = (try (Sys.getenv "OCAMLFIND_OK" ; true) with | _ -> false) 4 | let py = if ocamlfind_ok then Lymp.init "." else Lymp.init ~exec:"python3" ~ocamlfind:false ~lymppy_dirpath:"srcs" "." 5 | let builtin = Lymp.builtins py 6 | 7 | let make_stuff () = 8 | let file = Lymp.get_ref builtin "open" [Lymp.Pystr "test8.ml"] in 9 | () 10 | 11 | let () = 12 | print_endline "should show \"ok\" : " ; 13 | make_stuff () ; 14 | Lymp.close py ; 15 | Gc.full_major () ; 16 | print_endline "ok" -------------------------------------------------------------------------------- /examples/simple.ml: -------------------------------------------------------------------------------- 1 | 2 | 3 | open Lymp 4 | 5 | (* change "python3" to the name of your interpreter *) 6 | let interpreter = "python3" 7 | let py = init ~exec:interpreter "." 8 | let simple = get_module py "simple" 9 | 10 | let () = 11 | (* msg = simple.get_message() *) 12 | let msg = get_string simple "get_message" [] in 13 | let integer = get_int simple "get_integer" [] in 14 | let addition = get_int simple "sum" [Pyint 12 ; Pyint 10] in 15 | let strconcat = get_string simple "sum" [Pystr "first " ; Pystr "second"] in 16 | Printf.printf "%s\n%d\n%d\n%s\n" msg integer addition strconcat ; 17 | 18 | close py 19 | -------------------------------------------------------------------------------- /tests/test1.ml: -------------------------------------------------------------------------------- 1 | 2 | 3 | (* TESTING BASIC FUNCTION CALL *) 4 | 5 | let ocamlfind_ok = (try (Sys.getenv "OCAMLFIND_OK" ; true) with | _ -> false) 6 | let py = if ocamlfind_ok then Lymp.init "." else Lymp.init ~ocamlfind:false ~lymppy_dirpath:"srcs" "." 7 | let modul = Lymp.get_module py "modul" 8 | 9 | let rand_str () = 10 | Lymp.get_string modul "rand_str" [] 11 | 12 | let () = 13 | let str = Lymp.get_string modul "rand_str" [] in 14 | if str <> "salut les gars" then raise (Failure "failed") ; 15 | print_endline str ; 16 | 17 | let str = rand_str () in 18 | if str <> "salut les gars" then raise (Failure "failed") ; 19 | print_endline str ; 20 | 21 | Lymp.close py -------------------------------------------------------------------------------- /_tags: -------------------------------------------------------------------------------- 1 | # OASIS_START 2 | # DO NOT EDIT (digest: c4ed2b26dcfb6d5a5b888077fd80e49f) 3 | # Ignore VCS directories, you can use the same kind of rule outside 4 | # OASIS_START/STOP if you want to exclude directories that contains 5 | # useless stuff for the build process 6 | true: annot, bin_annot 7 | <**/.svn>: -traverse 8 | <**/.svn>: not_hygienic 9 | ".bzr": -traverse 10 | ".bzr": not_hygienic 11 | ".hg": -traverse 12 | ".hg": not_hygienic 13 | ".git": -traverse 14 | ".git": not_hygienic 15 | "_darcs": -traverse 16 | "_darcs": not_hygienic 17 | # Library lymp 18 | "srcs/lymp.cmxs": use_lymp 19 | : pkg_threads 20 | : pkg_unix 21 | # OASIS_STOP 22 | -------------------------------------------------------------------------------- /tests/test7.ml: -------------------------------------------------------------------------------- 1 | 2 | 3 | let ocamlfind_ok = (try (Sys.getenv "OCAMLFIND_OK" ; true) with | _ -> false) 4 | let py = if ocamlfind_ok then Lymp.init "." else Lymp.init ~exec:"python3" ~ocamlfind:false ~lymppy_dirpath:"srcs" "." 5 | let builtin = Lymp.builtins py 6 | 7 | let get_ints i = 8 | let string_repr1 = Lymp.get_string builtin "str" [Lymp.Pyint i] in 9 | let string_repr2 = Lymp.get_string builtin "str" [Lymp.Pyint (i + 100)] in 10 | let string_repr3 = Lymp.get_string builtin "str" [Lymp.Pyint (i + 200)] in 11 | Printf.printf "%s %s %s\n" string_repr1 string_repr2 string_repr3 12 | 13 | let () = 14 | let threads = ref [] in 15 | for i = 0 to 50 do 16 | threads := (Thread.create get_ints i)::(!threads) 17 | done ; 18 | List.iter Thread.join !threads ; 19 | Lymp.close py -------------------------------------------------------------------------------- /tests/test6.ml: -------------------------------------------------------------------------------- 1 | 2 | 3 | let ocamlfind_ok = (try (Sys.getenv "OCAMLFIND_OK" ; true) with | _ -> false) 4 | let py = if ocamlfind_ok then Lymp.init "." else Lymp.init ~exec:"python3" ~ocamlfind:false ~lymppy_dirpath:"srcs" "." 5 | let builtin = Lymp.builtins py 6 | 7 | let () = 8 | let file1 = Lymp.get_ref builtin "open" [Lymp.Pystr "test5.ml" ; Lymp.Namedarg ("encoding", Lymp.Pystr "utf-8")] in 9 | let encoding = Lymp.attr_string file1 "encoding" in 10 | if encoding = "utf-8" then () else raise (Failure "failed") ; 11 | let file2 = Lymp.get_ref builtin "open" [Lymp.Pystr "test5.ml" ; Lymp.Namedarg ("encoding", Lymp.Pystr "ascii")] in 12 | let encoding = Lymp.attr_string file2 "encoding" in 13 | if encoding = "ascii" then () else raise (Failure "failed") ; 14 | 15 | Lymp.close py -------------------------------------------------------------------------------- /examples/phantom.ml: -------------------------------------------------------------------------------- 1 | 2 | 3 | (* downloads a webpage using phantomjs, saves a screenshot of it to screen.png, 4 | selects links out of page, and prints the links' titles *) 5 | 6 | open Lymp 7 | 8 | let py = init "." 9 | let phantom = get_module py "phantom" 10 | 11 | let download_with_phantom url = 12 | get_string phantom "download" [Pystr url] 13 | 14 | let select html css_selector = 15 | get_list phantom "select" [Pystr html ; Pystr css_selector] 16 | 17 | let get_lxml_text (Pyref lxml_elt) = 18 | (* calling method text_content() of lxml element *) 19 | let text = get lxml_elt "text_content" [] in 20 | (* text is a custom lxml type, we convert it to str *) 21 | get_string (builtins py) "str" [text] 22 | 23 | let () = 24 | let url = "https://github.com/dbousque/lymp" in 25 | let page_content = download_with_phantom url in 26 | let links = select page_content "a" in 27 | let titles = List.map get_lxml_text links in 28 | List.iter print_endline titles ; 29 | 30 | close py 31 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # OASIS_START 2 | # DO NOT EDIT (digest: a3c674b4239234cbbe53afe090018954) 3 | 4 | SETUP = ocaml setup.ml 5 | 6 | build: setup.data 7 | $(SETUP) -build $(BUILDFLAGS) 8 | 9 | doc: setup.data build 10 | $(SETUP) -doc $(DOCFLAGS) 11 | 12 | test: setup.data build 13 | $(SETUP) -test $(TESTFLAGS) 14 | 15 | all: 16 | $(SETUP) -all $(ALLFLAGS) 17 | 18 | install: setup.data 19 | $(SETUP) -install $(INSTALLFLAGS) 20 | cp srcs/lymp.py `ocamlfind query lymp`/ 21 | pip install pymongo || echo "" 22 | pip3 install pymongo || echo "" 23 | 24 | uninstall: setup.data 25 | $(SETUP) -uninstall $(UNINSTALLFLAGS) 26 | 27 | reinstall: setup.data 28 | $(SETUP) -reinstall $(REINSTALLFLAGS) 29 | 30 | clean: 31 | $(SETUP) -clean $(CLEANFLAGS) 32 | 33 | distclean: 34 | $(SETUP) -distclean $(DISTCLEANFLAGS) 35 | 36 | setup.data: 37 | $(SETUP) -configure $(CONFIGUREFLAGS) 38 | 39 | configure: 40 | $(SETUP) -configure $(CONFIGUREFLAGS) 41 | 42 | .PHONY: build doc test all install uninstall reinstall clean distclean configure 43 | 44 | # OASIS_STOP 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Dominik Bousquet 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /benchmark/benchmark.ml: -------------------------------------------------------------------------------- 1 | 2 | 3 | let ocamlfind_ok = (try (Sys.getenv "OCAMLFIND_OK" ; true) with | _ -> false) 4 | let py = if ocamlfind_ok then Lymp.init "." else Lymp.init ~exec:"python3" ~ocamlfind:false ~lymppy_dirpath:"srcs" "." 5 | let modul = Lymp.get_module py "modul" 6 | 7 | let speed_test () = 8 | let nb = ref 0 in 9 | let start = Unix.gettimeofday () in 10 | while Unix.gettimeofday () -. start < 1.0 do 11 | ignore (Lymp.get_int modul "get_int" []) ; 12 | ignore (Lymp.get_int modul "get_int" []) ; 13 | ignore (Lymp.get_int modul "get_int" []) ; 14 | ignore (Lymp.get_int modul "get_int" []) ; 15 | ignore (Lymp.get_int modul "get_int" []) ; 16 | ignore (Lymp.get_int modul "get_int" []) ; 17 | ignore (Lymp.get_int modul "get_int" []) ; 18 | ignore (Lymp.get_int modul "get_int" []) ; 19 | ignore (Lymp.get_int modul "get_int" []) ; 20 | ignore (Lymp.get_int modul "get_int" []) ; 21 | nb := !nb + 10 22 | done ; 23 | print_endline ("Function call overhead : " ^ (string_of_int (1000000 / !nb)) ^ " μs") 24 | 25 | let () = 26 | speed_test () ; 27 | Lymp.close py -------------------------------------------------------------------------------- /srcs/LICENSE.bson.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright © 2013 3 | Marc Simon, marc.simon42@gmail.com, twitter.com/marcsimon42 4 | Dr. Xinuo Chen, iamindcs@gmail.com 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | -------------------------------------------------------------------------------- /examples/reference.ml: -------------------------------------------------------------------------------- 1 | 2 | 3 | (* example usage of an object through a reference (here a dict object) *) 4 | 5 | open Lymp 6 | 7 | let py = init "." 8 | let builtin = builtins py 9 | 10 | let () = 11 | (* create a dict *) 12 | let dict = get_ref builtin "dict" [] in 13 | (* dict["field1"] = "value1" *) 14 | call dict "__setitem__" [Pystr "field1" ; Pystr "value1"] ; 15 | call dict "__setitem__" [Pystr "field2" ; Pyint 2] ; 16 | call dict "__setitem__" [Pystr "field3" ; Pyfloat 3.3] ; 17 | (* getting fields, for example 'val1' is the string "value1" *) 18 | let val1 = get_string dict "get" [Pystr "field1"] in 19 | let val2 = get_int dict "get" [Pystr "field2"] in 20 | let val3 = get_float dict "get" [Pystr "field3"] in 21 | (* 'values' will be : [Pystr "value1" ; Pyint 2 ; Pyfloat 3.3] *) 22 | let values_ref = get dict "values" [] in 23 | (* my_dict.values() returns a 'dict_values' and not a 'list' in python 3, 24 | so we make a conversion with list(values_ref) *) 25 | let values = get_list builtin "list" [values_ref] in 26 | 27 | print_endline val1 ; 28 | print_endline (string_of_int val2) ; 29 | print_endline (string_of_float val3) ; 30 | 31 | print_endline (string_of_int (List.length values)) ; 32 | 33 | (* ouput will be : 34 | value1 35 | 2 36 | 3.3 37 | 3 38 | *) 39 | 40 | close py -------------------------------------------------------------------------------- /tests/test3.ml: -------------------------------------------------------------------------------- 1 | 2 | 3 | let ocamlfind_ok = (try (Sys.getenv "OCAMLFIND_OK" ; true) with | _ -> false) 4 | let py = if ocamlfind_ok then Lymp.init "." else Lymp.init ~exec:"python3" ~ocamlfind:false ~lymppy_dirpath:"srcs" "." 5 | let fetch = Lymp.get_module py "fetch" 6 | let builtin = Lymp.builtins py 7 | 8 | let () = 9 | let fetch_obj = Lymp.get_ref fetch "Fetch" [Lymp.Pystr "https://google.com" ; Lymp.Pystr "std"] in 10 | Lymp.call builtin "print" [Lymp.Pyref fetch_obj] ; 11 | let content = Lymp.get_string fetch_obj "download" [] in 12 | if content <> "

Hi

" then raise (Failure "failed") ; 13 | let obj_ref_again = Lymp.get_ref fetch_obj "ret_self" [] in 14 | let url = Lymp.attr_string fetch_obj "url" in 15 | if url <> "https://google.com" then raise (Failure "failed") ; 16 | let mode = Lymp.attr_string obj_ref_again "mode" in 17 | if mode <> "std" then raise (Failure "failed") ; 18 | 19 | let int_ref = Lymp.get_ref builtin "int" [Lymp.Pystr "42"] in 20 | let int_val = Lymp.dereference int_ref in 21 | (match int_val with 22 | | Lymp.Pyint v -> if v <> 42 then raise (Failure "failed") 23 | | _ -> raise (Failure "failed") ); 24 | let lst = Lymp.get_list fetch_obj "ret_list" [] in 25 | ( match lst with 26 | | [Lymp.Pyint i1 ; Lymp.Pyint i2 ; Lymp.Pyref obj ; Lymp.Pylist [Lymp.Pystr str ; Lymp.Pyint i3] ; Lymp.Pyint i4] -> if i1 <> 1 || i2 <> 2 || i3 <> 3 || i4 <> 4 || str <> "salut" then raise (Failure "failed") 27 | | _ -> raise (Failure "failed") ) ; 28 | 29 | Lymp.close py -------------------------------------------------------------------------------- /tests/test2.ml: -------------------------------------------------------------------------------- 1 | 2 | 3 | (* TESTING PYREF AND WRONGTYPE EXCEPTION *) 4 | 5 | let ocamlfind_ok = (try (Sys.getenv "OCAMLFIND_OK" ; true) with | _ -> false) 6 | let py = if ocamlfind_ok then Lymp.init "." else Lymp.init ~exec:"python3" ~ocamlfind:false ~lymppy_dirpath:"srcs" "." 7 | let modul = Lymp.get_module py "modul" 8 | 9 | let file_lines filename = 10 | let lines = ref [] in 11 | let chan = open_in filename in 12 | try 13 | while true; do 14 | lines := input_line chan :: !lines 15 | done ; 16 | !lines 17 | with End_of_file -> 18 | close_in chan ; 19 | List.rev !lines 20 | 21 | let rec check_lines lines expected_lines = 22 | match expected_lines with 23 | | [] -> () 24 | | e::rest_e -> match lines with 25 | | [] -> raise (Failure "failed") 26 | | l::rest_l -> if l <> e then raise (Failure "failed") else check_lines rest_l rest_e 27 | 28 | let () = 29 | ignore (try (Lymp.get_string modul "get_tuple" []) 30 | with Lymp.Wrong_Pytype _ -> "") ; 31 | (* tuples are converted to lists *) 32 | let tuple = Lymp.get modul "get_tuple" [] in 33 | ( match tuple with 34 | | Lymp.Pytuple l -> () 35 | | _ -> raise (Failure "failed")) ; 36 | 37 | (* ASSERTING THAT PASSING PYREF AS ARGUMENT PASSES ACTUAL OBJECT *) 38 | Lymp.call modul "print_tuple" [tuple] ; 39 | 40 | (* ASSERTING THAT GENERIC FUNCTIONS ACCEPTS ALL KINDS OF ARGUMENTS *) 41 | Lymp.call modul "print_arg" [Lymp.Pystr "salut"] ; 42 | Lymp.call modul "print_arg" [Lymp.Pyint 42] ; 43 | Lymp.call modul "print_arg" [Lymp.Pyfloat 42.42] ; 44 | Lymp.call modul "print_arg" [Lymp.Pybool true] ; 45 | Lymp.call modul "print_arg" [Lymp.Pybytes (Bytes.of_string "some bytes")] ; 46 | Lymp.call modul "print_arg" [Lymp.get modul "ret_unicode" []] ; 47 | Lymp.call modul "print_arg" [Lymp.get modul "rand_str" []] ; 48 | (* equivalent of modul.print_arg(modul.first_of_tuple(tuple)) : *) 49 | Lymp.call modul "print_arg" [Lymp.get modul "first_of_tuple" [tuple]] ; 50 | 51 | Lymp.close py ; 52 | 53 | (* python_log should now be : 54 | (1, 2) 55 | salut 56 | 42 57 | 42.42 58 | True 59 | b'some bytes' 60 | saluté 61 | alright, inside python 62 | salut les gars 63 | 1 64 | *) 65 | 66 | let lines = file_lines "python_log" in 67 | let expected_lines = [ 68 | "(1, 2)" ; 69 | "salut" ; 70 | "42" ; 71 | "42.42" ; 72 | "True" ; 73 | "b'some bytes'" ; 74 | "saluté" ; 75 | "alright, inside python" ; 76 | "salut les gars" ; 77 | "1" 78 | ] in 79 | check_lines lines expected_lines 80 | -------------------------------------------------------------------------------- /srcs/lymp.mli: -------------------------------------------------------------------------------- 1 | 2 | 3 | exception Unknown_return_type of string 4 | exception Wrong_Pytype of string 5 | exception Expected_reference_not_module 6 | exception Could_not_create_pipe 7 | exception Pyexception of string 8 | 9 | (* information enabling communication with the python process *) 10 | type pycommunication 11 | 12 | (* pycallable can be a module or a reference *) 13 | type pycallable 14 | 15 | (* arguments passed to python functions must be of type pyobj *) 16 | type pyobj = 17 | Pystr of string 18 | | Pyint of int 19 | | Pyfloat of float 20 | | Pybool of bool 21 | | Pybytes of bytes 22 | | Pyref of pycallable 23 | | Pytuple of pyobj list 24 | | Pylist of pyobj list 25 | | Pynone 26 | | Namedarg of (string * pyobj) 27 | 28 | val get_module : pycommunication -> string -> pycallable 29 | 30 | (* module givin access to builtins attributes and functions, such as 'print' *) 31 | val builtins : pycommunication -> pycallable 32 | 33 | (* call functions and methods of modules and objects *) 34 | val call : pycallable -> string -> pyobj list -> unit 35 | val get : pycallable -> string -> pyobj list -> pyobj 36 | val get_string : pycallable -> string -> pyobj list -> string 37 | val get_int : pycallable -> string -> pyobj list -> int 38 | val get_float : pycallable -> string -> pyobj list -> float 39 | val get_bool : pycallable -> string -> pyobj list -> bool 40 | val get_bytes : pycallable -> string -> pyobj list -> bytes 41 | val get_ref : pycallable -> string -> pyobj list -> pycallable 42 | val get_tuple : pycallable -> string -> pyobj list -> pyobj list 43 | val get_list : pycallable -> string -> pyobj list -> pyobj list 44 | 45 | (* get attributes of modules and objects *) 46 | val attr : pycallable -> string -> pyobj 47 | val attr_string : pycallable -> string -> string 48 | val attr_int : pycallable -> string -> int 49 | val attr_float : pycallable -> string -> float 50 | val attr_bool : pycallable -> string -> bool 51 | val attr_bytes : pycallable -> string -> bytes 52 | val attr_ref : pycallable -> string -> pycallable 53 | val attr_tuple : pycallable -> string -> pyobj list 54 | val attr_list : pycallable -> string -> pyobj list 55 | 56 | val set_attr : pycallable -> string -> pyobj -> unit 57 | 58 | (* get what is being referenced, 59 | will return a Pyref if the python type is not supported *) 60 | val dereference : pycallable -> pyobj 61 | 62 | val close : pycommunication -> unit 63 | val init : ?exec:string -> ?ocamlfind:bool -> ?lymppy_dirpath:string -> string -> pycommunication 64 | -------------------------------------------------------------------------------- /srcs/bson.mli: -------------------------------------------------------------------------------- 1 | (** 2 | This module includes a Bson document data structure, together with its encoding (to bytes) and decoding (from bytes). 3 | 4 | The logic of {b usage} is like this 5 | - Create an empty Bson document 6 | - Create the elements you want 7 | - Add elements to the document with names 8 | - Or remove elements from the document via the names 9 | - Get elements from the document via the names 10 | - After obtaining an element, get the raw value from the element 11 | 12 | The functions inside this module seem to be many, however, most of them are just for creating elements. These functions are to {e hide the implementation details of the type elements}. Also, in this way, the Bson document can be used more safely. 13 | 14 | Please refer to the {{: http://bsonspec.org/#/specification } Official Bson specification } for more information. 15 | 16 | {e Version 0.89.1} 17 | *) 18 | 19 | (** Raised when an objectId's length is not 12. see http://bsonspec.org/#/specification *) 20 | exception Invalid_objectId;; 21 | 22 | (** Raised when an unkown bson type is met while encoding the bson doc *) 23 | exception Wrong_bson_type;; 24 | 25 | (** Raised only when trying to decode the bytes to string. *) 26 | exception Wrong_string;; 27 | 28 | (** Raised when bad things happen while decoding the bytes to bson doc *) 29 | exception Malformed_bson;; 30 | 31 | (** The type for the Bson document. This is the main data structure *) 32 | type t;; 33 | 34 | (** The type for representing the special fields in Bson *) 35 | type special = 36 | | NULL 37 | | MINKEY 38 | | MAXKEY;; 39 | 40 | (** The type for the fields for the Bson document *) 41 | type element;; 42 | 43 | 44 | (** {6 Basic operations on Bson document} *) 45 | 46 | (** The empty Bson document *) 47 | val empty : t;; 48 | 49 | (** Check whether this Bson document empty or not *) 50 | val is_empty: t -> bool;; 51 | 52 | (** Add an element to a Bson document *) 53 | val add_element : string -> element -> t -> t;; 54 | 55 | (** Get an element from a Bson document via its name *) 56 | val get_element : string -> t -> element;; 57 | 58 | (** Check whether this Bson document has a specific element *) 59 | val has_element : string -> t -> bool;; 60 | 61 | (** Remove an element from a Bson document *) 62 | val remove_element : string -> t -> t;; 63 | 64 | (** Encode a Bson document to bytes (using type string as a carrier) *) 65 | val encode : t -> string;; 66 | 67 | (** Decode bytes (assuming type string as a carrier) to a Bson document *) 68 | val decode : string -> t;; 69 | 70 | 71 | (** {6 Creating elements} *) 72 | 73 | 74 | val create_double : float -> element;; 75 | val create_string : string -> element;; 76 | val create_doc_element : t -> element;; 77 | val create_list : element list -> element;; 78 | val create_doc_element_list : t list -> element;; 79 | val create_user_binary : string -> element;; 80 | val create_objectId : string -> element;; 81 | val create_boolean : bool -> element;; 82 | val create_utc : int64 -> element;; 83 | val create_null : unit -> element;; 84 | val create_regex : string -> string -> element;; 85 | val create_jscode : string -> element;; 86 | val create_jscode_w_s : string -> t -> element;; 87 | val create_int32 : int32 -> element;; 88 | val create_int64 : int64 -> element;; 89 | val create_minkey : unit -> element;; 90 | val create_maxkey : unit -> element;; 91 | 92 | 93 | (** {6 Getting raw values from elements} *) 94 | 95 | 96 | val get_double : element -> float;; 97 | val get_string : element -> string;; 98 | val get_doc_element : element -> t;; 99 | val get_list : element -> element list;; 100 | val get_generic_binary : element -> string;; 101 | val get_function_binary : element -> string;; 102 | val get_uuid_binary : element -> string;; 103 | val get_md5_binary : element -> string;; 104 | val get_user_binary : element -> string;; 105 | val get_objectId : element -> string;; 106 | val get_boolean : element -> bool;; 107 | val get_utc : element -> int64;; 108 | val get_null : element -> special;; 109 | val get_regex : element -> (string * string);; 110 | val get_jscode : element -> string;; 111 | val get_jscode_w_s : element -> (string * t);; 112 | val get_int32 : element -> int32;; 113 | val get_int64 : element -> int64;; 114 | val get_timestamp : element -> int64;; 115 | val get_minkey : element -> special;; 116 | val get_maxkey : element -> special;; 117 | 118 | val all_elements : t -> (string * element) list 119 | 120 | (** {6 Experimental. Convert a Bson document to Json.} *) 121 | 122 | 123 | val to_simple_json : t -> string;; 124 | 125 | (*val create_generic_binary : string -> element;; 126 | val create_function_binary : string -> element;; 127 | val create_uuid_binary : string -> element;; 128 | val create_md5_binary : string -> element;; 129 | val create_timestamp : int64 -> element;;*) 130 | -------------------------------------------------------------------------------- /srcs/lymp.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from time import time 4 | from struct import pack, unpack 5 | import bson, sys, os, codecs 6 | from random import randint 7 | from traceback import print_exc 8 | 9 | def int_to_int64_bytes(i): 10 | return pack('>q', i) 11 | 12 | def py_to_bson(val): 13 | if type(val) is int: 14 | return bson.int64.Int64(val) 15 | if sys.version_info.major == 2 and type(val) is str: 16 | return bson.binary.Binary(val) 17 | return val 18 | 19 | def exit_lymp(): 20 | # closing 'python_log' 21 | sys.stdout.close() 22 | exit(0) 23 | 24 | # A communication class, could be implemented using other ipc methods, 25 | # it only needs the methods 'send_bytes' and 'get_bytes' 26 | class PipeReaderWriter: 27 | 28 | def __init__(self, read_pipe_name, write_pipe_name): 29 | self.get_pipes(read_pipe_name, write_pipe_name) 30 | 31 | def get_pipes(self, read_pipe_name, write_pipe_name): 32 | # Order of open matters, since it is blocking, should match OCaml order 33 | # 0 to be unbuffered, so we don't have to flush (better performance ?) 34 | self.write_pipe = open(write_pipe_name, 'wb', 0) 35 | self.read_pipe = open(read_pipe_name, 'rb', 0) 36 | 37 | def send_bytes(self, byts): 38 | # '>q' to force signed 8 bytes integer 39 | self.write_pipe.write(pack('>q', len(byts))) 40 | #self.write_pipe.flush() 41 | self.write_pipe.write(byts) 42 | #self.write_pipe.flush() 43 | 44 | def get_bytes(self): 45 | # '>q' to force signed 8 bytes integer 46 | try: 47 | nb_bytes = unpack('>q', self.read_pipe.read(8))[0] 48 | except: 49 | # ocaml process has been terminated 50 | exit_lymp() 51 | byts = b'' if sys.version_info.major == 3 else "" 52 | while len(byts) < nb_bytes: 53 | byts += self.read_pipe.read(nb_bytes) 54 | return byts 55 | 56 | class ExecutionHandler: 57 | 58 | to_ret_types = { 59 | int: "i", 60 | tuple: "t", 61 | list: "l", 62 | str: "s", 63 | float: "f", 64 | type(None): "n", 65 | bool: "b", 66 | bytes: "B" 67 | } 68 | # for python 2, unicode is str and str is bytes 69 | if sys.version_info.major == 2: 70 | to_ret_types[unicode] = "s" 71 | to_ret_types[str] = "B" 72 | 73 | def __init__(self, reader_writer): 74 | self.reader_writer = reader_writer 75 | self.modules = {} 76 | self.objs = {} 77 | self.ref_nb = 0 78 | 79 | def loop(self): 80 | # don't recursively call .loop, to avoid stack overflow 81 | while True: 82 | command_bytes = self.reader_writer.get_bytes() 83 | if command_bytes == b'done': 84 | exit_lymp() 85 | instruction = bson.BSON.decode(bson.BSON(command_bytes)) 86 | try: 87 | ret = self.execute_instruction(instruction) 88 | # data may still be in the buffer 89 | sys.stdout.flush() 90 | self.send_ret(ret, ret_ref=("R" in instruction)) 91 | except BaseException as e: 92 | # exception whilst executing, inform ocaml side 93 | print_exc() 94 | # data may still be in the buffer 95 | sys.stdout.flush() 96 | self.send_ret("", exception=True) 97 | 98 | def ret_to_msg(self, ret, ret_ref): 99 | msg = {} 100 | # reference (type not supported or explicitely asked to) 101 | if ret_ref or (type(ret) not in self.to_ret_types): 102 | self.ref_nb += 1 103 | self.objs[self.ref_nb] = ret 104 | msg["t"] = "r" 105 | msg["v"] = bson.code.Code(str(self.ref_nb)) 106 | else: 107 | msg["t"] = self.to_ret_types[type(ret)] 108 | # tuples are just like lists, but their type "t" is "t" instead of "l" 109 | if type(ret) is tuple: 110 | ret = list(ret) 111 | # if type is list, further resolve 112 | if type(ret) is list: 113 | msg["v"] = [] 114 | for elt in ret: 115 | # ret_ref is false here (would not be in the else otherwise) 116 | msg["v"].append(self.ret_to_msg(elt, False)) 117 | else: 118 | msg["v"] = py_to_bson(ret) 119 | return msg 120 | 121 | def send_ret(self, ret, exception=False, ret_ref=False): 122 | if exception: 123 | msg = {} 124 | msg["t"] = "e" 125 | msg["v"] = "" 126 | else: 127 | msg = self.ret_to_msg(ret, ret_ref) 128 | msg = bytes(bson.BSON.encode(msg)) 129 | self.reader_writer.send_bytes(msg) 130 | 131 | def resolve_args(self, args): 132 | named = {} 133 | i = 0 134 | for arg in args: 135 | # resolve named args (list of size 2, first one being a bson.code.Code starting with "!") 136 | if type(arg) is list and len(arg) == 2 and type(arg[0]) is bson.code.Code and str(arg[0])[0] == "!": 137 | named[str(arg[0])[1:]] = self.resolve_args([arg[1]])[0][0] 138 | del args[i] 139 | continue 140 | # if bytes 141 | if type(arg) is bson.binary.Binary: 142 | args[i] = bytes(arg) 143 | # resolve reference args (using bson jscode) 144 | if type(arg) is bson.code.Code: 145 | args[i] = self.objs[int(arg)] 146 | if type(arg) is bson.int64.Int64: 147 | args[i] = int(arg) 148 | # for python 2, if arg is str, convert to unicode 149 | if sys.version_info.major == 2 and type(arg) is str: 150 | args[i] = args[i].decode('utf-8') 151 | # for python 2, if arg is bytes, convert to str 152 | if sys.version_info.major == 2 and type(arg) is bson.binary.Binary: 153 | args[i] = str(arg) 154 | # if we have a list, we must recursively resolve 155 | if type(arg) is list: 156 | args[i] = self.resolve_args(arg)[0] 157 | # if we have a dict, it is a tuple inside "v" 158 | if type(arg) is dict: 159 | args[i] = tuple(self.resolve_args(arg["v"])[0]) 160 | i += 1 161 | return args, named 162 | 163 | def execute_instruction(self, instruction): 164 | if "r" in instruction: 165 | # if we are asked to realease an inexisting of already released reference 166 | if "d" in instruction and instruction["r"] not in self.objs: 167 | return None 168 | # module is the object referenced, later we call getattr to get the method called 169 | module = self.objs[instruction["r"]] 170 | # if we were asked to 'detach' (release) the reference 171 | if "d" in instruction: 172 | del self.objs[instruction["r"]] 173 | return None 174 | # if we were asked to return the reference 175 | if "g" in instruction: 176 | return module 177 | else: 178 | # python 2 builtin module has a different name 179 | if sys.version_info.major == 2 and instruction["m"] == "builtins": 180 | instruction["m"] = "__builtin__" 181 | if instruction["m"] not in self.modules: 182 | __import__(instruction["m"]) 183 | self.modules[instruction["m"]] = sys.modules[instruction["m"]] 184 | module = self.modules[instruction["m"]] 185 | # set attribute 186 | if "s" in instruction: 187 | args, named = self.resolve_args(instruction["a"]) 188 | arg = args[0] 189 | setattr(module, instruction["f"], arg) 190 | return None 191 | func_or_attr = getattr(module, instruction["f"]) 192 | # get attribute 193 | if "t" in instruction: 194 | return func_or_attr 195 | args = instruction["a"] 196 | args, named = self.resolve_args(args) 197 | ret = func_or_attr(*args, **named) 198 | return ret 199 | 200 | working_directory = sys.argv[1] 201 | write_pipe_path = sys.argv[2] 202 | read_pipe_path = sys.argv[3] 203 | # changing dir 204 | os.chdir(working_directory) 205 | sys.path.insert(0, working_directory) 206 | # redirect stdout to 'python_log' 207 | sys.stdout = codecs.open('python_log', 'w', encoding='utf-8') 208 | sys.stderr = sys.stdout 209 | communication = PipeReaderWriter(read_pipe_path, write_pipe_path) 210 | handler = ExecutionHandler(communication) 211 | handler.loop() 212 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Lymp

2 | 3 | `lymp` is a library allowing you to use Python functions and objects from OCaml. It gives access to the rich ecosystem of libraries in Python. You might want to use `selenium`, `scipy`, `lxml`, `requests`, `tensorflow` or `matplotlib`. 4 | 5 | You can also very easily write OCaml wrappers for Python libraries or your own modules. 6 | 7 | Python 2 and 3 compatible. Thread safe. 8 | 9 |

Installation and compilation

10 | 11 | `opam install lymp` 12 | 13 | Python's `pymongo` package is required (for it's bson subpackage), `opam` and the Makefile try to install it using `pip` and `pip3`, so you should not have to install it manually. If `$ python3 -c "import pymongo"` fails, you need to install `pymongo`, maybe using sudo on `pip` or `pip3`. 14 | 15 | To make sure everything is fine, you may want to compile the simple example, like so for example : `ocamlbuild -use-ocamlfind -pkgs lymp -tag thread simple.native && ./simple.native` 16 | 17 | When compiling a project using `lymp`, you need to link the `thread` library. For example, when using ocamlbuild, set a tag : `-tag thread`. 18 | 19 | If you have trouble building the package, please contact me. 20 | 21 |

Simple example

22 | 23 | ``` 24 | $ ls 25 | simple.ml 26 | simple.py 27 | ``` 28 |

simple.py

29 | 30 | ```python 31 | def get_message(): 32 | return u"hi there" 33 | 34 | def get_integer(): 35 | return 42 36 | 37 | def sum(a, b): 38 | return a + b 39 | ``` 40 | 41 |

simple.ml

42 | 43 | ```ocaml 44 | open Lymp 45 | 46 | (* change "python3" to the name of your interpreter *) 47 | let interpreter = "python3" 48 | let py = init ~exec:interpreter "." 49 | let simple = get_module py "simple" 50 | 51 | let () = 52 | (* msg = simple.get_message() *) 53 | let msg = get_string simple "get_message" [] in 54 | let integer = get_int simple "get_integer" [] in 55 | let addition = get_int simple "sum" [Pyint 12 ; Pyint 10] in 56 | let strconcat = get_string simple "sum" [Pystr "first " ; Pystr "second"] in 57 | Printf.printf "%s\n%d\n%d\n%s\n" msg integer addition strconcat ; 58 | 59 | close py 60 | ``` 61 | 62 | ``` 63 | $ ./simple.native 64 | hi there 65 | 42 66 | 22 67 | first second 68 | ``` 69 | 70 |

Useful example

71 | 72 | This example shows how you can use `selenium` and `lxml` to download a webpage (with content loaded via Javascript thanks to PhantomJS), and then parse it and manipulate the DOM. You would need `lxml`, `cssselect`, `selenium`, nodeJS and phantomJS (through `npm` for example) to run this example. 73 | 74 |

phantom.py

75 | 76 | ```python 77 | import lxml.html as lx 78 | from selenium import webdriver 79 | 80 | driver = webdriver.PhantomJS() 81 | driver.set_window_size(1024, 768) 82 | 83 | def download(url): 84 | driver.get(url) 85 | driver.save_screenshot('screen.png') 86 | return driver.page_source 87 | 88 | def select(html, css_selector): 89 | doc = lx.fromstring(html) 90 | return doc.cssselect(css_selector) 91 | ``` 92 | 93 |

phantom.ml

94 | 95 | ```ocaml 96 | (* downloads a webpage using phantomjs, saves a screenshot of it to screen.png, 97 | selects links out of the page, and prints the links' titles *) 98 | 99 | open Lymp 100 | 101 | let py = init "." 102 | let phantom = get_module py "phantom" 103 | 104 | let download_with_phantom url = 105 | get_string phantom "download" [Pystr url] 106 | 107 | let select html css_selector = 108 | get_list phantom "select" [Pystr html ; Pystr css_selector] 109 | 110 | let get_lxml_text (Pyref lxml_elt) = 111 | (* calling method text_content() of lxml element *) 112 | let text = get lxml_elt "text_content" [] in 113 | (* text is a custom lxml type, we convert it to str *) 114 | get_string (builtins py) "str" [text] 115 | 116 | let () = 117 | let url = "https://github.com/dbousque/lymp" in 118 | let page_content = download_with_phantom url in 119 | let links = select page_content "a" in 120 | let titles = List.map get_lxml_text links in 121 | List.iter print_endline titles ; 122 | 123 | close py 124 | ``` 125 | You don't really need the python script to do that, you could write it completely in OCaml using `lymp`, getting and manipulating the `driver` object directly using a reference. 126 | 127 |

pyobj

128 | 129 | ```ocaml 130 | type pyobj = 131 | Pystr of string 132 | | Pyint of int 133 | | Pyfloat of float 134 | | Pybool of bool 135 | | Pybytes of bytes 136 | | Pyref of pycallable 137 | | Pytuple of pyobj list 138 | | Pylist of pyobj list 139 | | Pynone 140 | | Namedarg of (string * pyobj) 141 | ``` 142 | 143 | Main type representing python values, which are passed as arguments of functions and returned from functions. `Pyref` allows us to use python objects, we explain that later on. 144 | 145 | `Namedarg` represents a named argument, which you can use like so : 146 | ```ocaml 147 | get builtin "open" [Pystr "input.txt" ; Namedarg ("encoding", Pystr "utf-8")] 148 | ``` 149 | 150 |

API

151 | 152 | `init` spawns a Python process and gets it ready. A `pycommunication` is returned, which you can then use to make modules. `get_module` can be thought of as an `import` statement in Python. 153 | You can then call the functions or get the attributes of the module, using the get* and attr* functions. 154 | 155 |
156 | 157 | ```ocaml 158 | val init : ?exec:string -> ?ocamlfind:bool -> ?lymppy_dirpath:string -> string -> pycommunication 159 | ``` 160 | - 1. `exec` : name of the python interpreter, or path to it. Default is `python3` (python 2 and 3 are both supported) 161 | - 2. `ocamlfind` : `lymp` uses a python script, `lymp.py`, which is in `ocamlfind query lymp` if you installed through opam or the Makefile. If you didn't install that way, set `ocamlfind` to `false`. Default is `true` 162 | - 3. `lymppy_dirpath` : if `ocamlfind` is set to `false`, `lymp.py` will be assumed to be in `lymppy_dirpath`. Default is `"."` 163 | - 4. path from which python will be launched, which influences what modules are accessible. Example value : `"../py_utils"` 164 | 165 |
166 | 167 | ```ocaml 168 | val get_module : pycommunication -> string -> pycallable 169 | ``` 170 | - 1. a value returned by `init` 171 | - 2. name of the module you wish to use (can be something like `"app.crypto.utils"`) 172 | 173 |
174 | 175 | ```ocaml 176 | val builtins : pycommunication -> pycallable 177 | ``` 178 | - 1. a value returned by `init` 179 | 180 | Returns the module giving access to built-in functions and attributes, such as `print()`, `str()`, `dir()` etc. 181 | 182 |
183 | 184 | ```ocaml 185 | val get : pycallable -> string -> pyobj list -> pyobj 186 | ``` 187 | - 1. a module or a reference, from which you wish to call a function 188 | - 2. name of the function 189 | - 3. arguments of the function 190 | 191 | Example : `get time "sleep" [Pyint 2]` (equivalent in python : `time.sleep(2)`) 192 | 193 | Sister functions : `get_string`, `get_int`, `get_float`, `get_bool`, `get_bytes`, `get_tuple` and `get_list`. They call `get` and try to do pattern matching over the result to return the desired type, they fail with a `Wrong_Pytype` if the result was not from the expected type. For example, `get_string` doesn't return a `pyobj`, but a `string`. 194 | 195 |
196 | 197 | ```ocaml 198 | val call : pycallable -> string -> pyobj list -> unit 199 | ``` 200 | 201 | Calls `get` and dismisses the value returned 202 | 203 |
204 | 205 | ```ocaml 206 | val attr : pycallable -> string -> pyobj 207 | ``` 208 | - 1. a module or a reference, from which you wish to get an attribute 209 | - 2. name of the attribute 210 | 211 | Example : `attr sys "argv"` (equivalent in python : `sys.argv`) 212 | 213 | Sister functions : `attr_string`, `attr_int`, `attr_float`, `attr_bool`, `attr_bytes`, `attr_tuple` and `attr_list`. They call `attr` and try to do pattern matching over the result to return the desired type, they fail with a `Wrong_Pytype` if the result was not from the expected type. 214 | 215 |
216 | 217 | ```ocaml 218 | val set_attr : pycallable -> string -> pyobj -> unit 219 | ``` 220 | - 1. a module or a reference, to which you wish to set an attribute 221 | - 2. name of the attribute 222 | - 3. value to set the attribute to 223 | 224 | Example : `set_attr sys "stdout" (Pyint 42)` (equivalent in python : `sys.stdout = 42`) 225 | 226 |
227 | 228 | ```ocaml 229 | val close : pycommunication -> unit 230 | ``` 231 | - 1. a value returned by `init` 232 | 233 | Exit properly, it's important to call it. 234 | 235 | 236 |

References

237 | To be able to use python objects of non supported-types (anything outside of int, str etc.), we have references. 238 | 239 | A `Pyreference` is of type `pycallable`, which allows us to call `get` and `attr` on it. When passed as arguments or returned from functions, they are passed as `Pyref`, of type `pyobj`. 240 | 241 | References passed as arguments are resolved on the python side, which means that if you call a function with a reference as argument, on the python side the actual object will be passed. 242 | 243 | Another use case for references (other than unsupported types) is for very big strings, bytes or lists, which you may not wish to send back and forth between OCaml and Python if you need to further process them in python. Passing is relatively cheap, but you may want to avoid it. 244 | 245 | Objects referenced are garbage collected when you no longer need them. 246 | 247 |
248 | 249 | ```ocaml 250 | val get_ref : pycallable -> string -> pyobj list -> pycallable 251 | ``` 252 | Calls `get` and forces the result to be a reference, so the actual data is not sent back to OCaml, but remains on the Python side. To be used for unsupported types and big strings, bytes and lists if you need to further process them in python. What we call "big string" is a whole webpage for example (but as shown in the "Useful example", it's perfectly fine to pass the string directly back and forth). 253 | 254 |
255 | 256 | ```ocaml 257 | val attr_ref : pycallable -> string -> pycallable 258 | ``` 259 | Calls `attr` and forces the result to be a reference. 260 | 261 |
262 | 263 | ```ocaml 264 | val dereference : pycallable -> pyobj 265 | ``` 266 | If the value's type is supported, it will be returned, otherwise a reference to it is returned. 267 | 268 |
269 | 270 | Example usage of a reference : 271 | ```ocaml 272 | let file = get_ref builtin "open" [Pystr "input_file.txt"] in 273 | call builtin "print" [Pyref file] ; 274 | let content = get_string file "read" [] in 275 | print_endline content 276 | ``` 277 | You can find a more in-depth example in `examples/reference.ml` 278 | 279 |

Notes

280 | 281 | - In Python 2, Pystr are converted to `unicode`, assuming that the string is utf-8 encoded, and Pybytes to `str` 282 | - If there is a fatal exception, the python process continues as normal, but a Pyexception is raised on the OCaml side. 283 | - Python's stdout is a file named `python_log`, you will find the output and uncatched exceptions' traceback there. 284 | - Python's `int`s are converted to OCaml `int`s, overflow and underflow are therefore possible. Same goes for `float`. 285 | 286 |

Implementation

287 | 288 | `lymp` currently uses named pipes to make OCaml and Python processes communicate. BSON is used to serialize data passed. 289 | Performance is very good for almost all use cases. On my setup (virtual machine and relatively low specs), the overhead associated with a function call is roughly 25 μs. You can launch the benchmark to see what the overhead is on yours. 290 | Performance could be improved by using other IPC methods, such as shared memory. 291 | 292 |

"lymp" ?

293 | "pyml" was already taken, and so were "ocpy" and "pyoc", so I figured I would just mix letters. 294 | 295 |

TODO

296 | 297 | If it matters to you, better support for Python exceptions could be implemented (currently, a Pyexception is raised). Also, better performance would be pretty easy to get. Support for dicts could be added. We could also add the option to log Python's stdout to OCaml's stdout (there would be some drawbacks but it might be worth it). You are welcome to make pull requests and suggestions. 298 | -------------------------------------------------------------------------------- /srcs/lymp.ml: -------------------------------------------------------------------------------- 1 | 2 | 3 | exception Unknown_return_type of string 4 | exception Wrong_Pytype of string 5 | exception Expected_reference_not_module 6 | exception Could_not_create_pipe 7 | exception Pyexception of string 8 | 9 | 10 | (* TYPES *) 11 | 12 | type pipe = { 13 | path: string ; 14 | fd: Unix.file_descr 15 | } 16 | 17 | type pycommunication = { 18 | read_pipe: pipe ; 19 | write_pipe: pipe ; 20 | process_in: in_channel ; 21 | process_out: out_channel ; 22 | mutex: Mutex.t ; 23 | mutable closed: bool 24 | } 25 | 26 | type reference = { 27 | mutable py: pycommunication ; 28 | ref_nb: int ; 29 | mutable released: bool 30 | } 31 | 32 | type pycallable = 33 | Pymodule of pycommunication * string 34 | | Pyreference of reference 35 | 36 | type pyobj = 37 | Pystr of string 38 | | Pyint of int 39 | | Pyfloat of float 40 | | Pybool of bool 41 | | Pybytes of bytes 42 | | Pyref of pycallable 43 | | Pytuple of pyobj list 44 | | Pylist of pyobj list 45 | | Pynone 46 | | Namedarg of (string * pyobj) 47 | 48 | let ret_wrongtype mod_name func_name expected_type returned_type = 49 | let str = mod_name ^ "." ^ func_name ^ " : " in 50 | Wrong_Pytype (str ^ "expected " ^ expected_type ^ " but python returned " ^ returned_type) 51 | 52 | let make_wrongtype callable func_name expected_type ret_obj = 53 | let type_ret = ( match ret_obj with 54 | | Pystr _ -> "str" 55 | | Pyint _ -> "int" 56 | | Pyfloat _ -> "float" 57 | | Pybool _ -> "bool" 58 | | Pybytes _ -> "bytes" 59 | | Pyref _ -> "Pyref" 60 | | Pytuple _ -> "tuple" 61 | | Pylist _ -> "list" 62 | | Pynone -> "Nonetype" 63 | | Namedarg _ -> "Namedarg" 64 | ) in 65 | let mod_name = (match callable with 66 | | Pymodule (_,mod_name) -> mod_name 67 | | _ -> "method " 68 | ) in 69 | ret_wrongtype mod_name func_name expected_type type_ret 70 | 71 | 72 | (* COMMUNICATION UTILS *) 73 | 74 | let get_random_characters () = 75 | String.init 10 (fun i -> Char.chr (Random.int 26 + Char.code 'a')) 76 | 77 | let rec create_pipe path name = 78 | let rand_name = get_random_characters () in 79 | let rand_path = (path ^ rand_name) in 80 | if Sys.file_exists rand_path then create_pipe path name 81 | else ( 82 | (try Unix.mkfifo rand_path 0o600 with 83 | | Unix.Unix_error _ -> raise Could_not_create_pipe) ; 84 | name ^ rand_name 85 | ) 86 | 87 | let get_pipes path_read path_write = 88 | (* set O_SYNC to have synchronous (unbuffered) communication, 89 | so we don't have to flush, maybe O_DSYNC instead ? *) 90 | let fd_read = Unix.openfile path_read [Unix.O_RDONLY; Unix.O_SYNC] 0o600 in 91 | let fd_write = Unix.openfile path_write [Unix.O_WRONLY; Unix.O_SYNC] 0o600 in 92 | ({path = path_read ; fd = fd_read}, {path = path_write ; fd = fd_write}) 93 | 94 | let create_process exec pyroot ocamlfind_ready lymppy_dirpath read_pipe_name write_pipe_name = 95 | let path = ( 96 | if ocamlfind_ready then 97 | "`ocamlfind query lymp`" ^ Filename.dir_sep 98 | else 99 | lymppy_dirpath ^ Filename.dir_sep 100 | ) in 101 | let command = exec ^ " " ^ path ^ "lymp.py " in 102 | let command = command ^ "$(cd " ^ pyroot ^ " ; pwd) " in 103 | let command = command ^ "$(cd " ^ pyroot ^ " ; pwd)" ^ Filename.dir_sep ^ read_pipe_name ^ " " in 104 | let command = command ^ "$(cd " ^ pyroot ^ " ; pwd)" ^ Filename.dir_sep ^ write_pipe_name in 105 | Unix.open_process (command) 106 | 107 | 108 | (* SERIALIZATION / DESERIALIZATION *) 109 | 110 | let int64_mod i n = 111 | Int64.sub i (Int64.mul (Int64.div i (Int64.of_int n)) (Int64.of_int n)) 112 | 113 | let int64_to_bytes i = 114 | let bytes = Bytes.make 8 (Char.chr 0) in 115 | let rec _to_bytes bytes i ind = 116 | Bytes.set bytes ind (Char.chr (Int64.to_int (int64_mod i 256))) ; 117 | match ind with 118 | | 0 -> () 119 | | n -> _to_bytes bytes (Int64.div i (Int64.of_int 256)) (n - 1) 120 | in 121 | _to_bytes bytes i 7 ; 122 | bytes 123 | 124 | let bytes_to_int bytes nb = 125 | let rec _to_int bytes ind nb ret = 126 | let tmp = Char.code (Bytes.get bytes ind) in 127 | match nb - ind - 1 with 128 | | 0 -> ret * 256 + tmp 129 | | _ -> _to_int bytes (ind + 1) nb (ret * 256 + tmp) 130 | in 131 | _to_int bytes 0 nb 0 132 | 133 | let send_raw_bytes py bytes = 134 | let len = Int64.of_int (Bytes.length bytes) in 135 | ignore (Unix.write py.write_pipe.fd (int64_to_bytes len) 0 8) ; 136 | ignore (Unix.write py.write_pipe.fd bytes 0 (Bytes.length bytes)) 137 | 138 | let get_raw_bytes py = 139 | let len = Bytes.make 8 (Char.chr 0) in 140 | ignore (Unix.read py.read_pipe.fd len 0 8) ; 141 | let to_read = bytes_to_int len 8 in 142 | let ret_bytes = Bytes.make to_read (Char.chr 0) in 143 | let nb_read = ref 0 in 144 | while !nb_read < to_read do 145 | nb_read := !nb_read + Unix.read py.read_pipe.fd ret_bytes !nb_read (to_read - !nb_read) 146 | done ; 147 | ret_bytes 148 | 149 | let compose f g x = f (g x) 150 | 151 | (* Bson.encode reverses lists, so use rev_map *) 152 | let rec serialize_list lst = 153 | Bson.create_list (List.rev_map serialize lst) 154 | 155 | and serialize = function 156 | | Pystr str -> Bson.create_string str 157 | | Pyint i -> Bson.create_int64 (Int64.of_int i) 158 | | Pytuple lst -> Bson.empty |> Bson.add_element "v" (serialize_list lst) |> Bson.create_doc_element 159 | | Pylist lst -> serialize_list lst 160 | | Pyfloat f -> Bson.create_double f 161 | | Pybool b -> Bson.create_boolean b 162 | | Pybytes b -> Bson.create_user_binary (Bytes.to_string b) 163 | | Pyref (Pyreference {py ; ref_nb ; released}) -> Bson.create_jscode (string_of_int ref_nb) 164 | | Pyref (Pymodule _) -> raise Expected_reference_not_module 165 | | Pynone -> Bson.create_null () 166 | | Namedarg (name, value) -> Bson.create_list [serialize value ; Bson.create_jscode ("!" ^ name)] 167 | 168 | (* Bson.decode reverses lists, so use rev_map *) 169 | let rec deserialize_list py lst = 170 | List.rev_map (compose (deserialize py) Bson.get_doc_element) lst 171 | 172 | and deserialize py doc = 173 | let element = Bson.get_element "v" doc in 174 | match Bson.get_string (Bson.get_element "t" doc) with 175 | | "s" -> Pystr (Bson.get_string element) 176 | | "i" -> Pyint (Int64.to_int (Bson.get_int64 element)) 177 | | "t" -> Pytuple (deserialize_list py (Bson.get_list element)) 178 | | "l" -> Pylist (deserialize_list py (Bson.get_list element)) 179 | | "f" -> Pyfloat (Bson.get_double element) 180 | | "b" -> Pybool (Bson.get_boolean element) 181 | | "B" -> Pybytes ((Bson.get_generic_binary element) |> Bytes.of_string) 182 | | "r" -> let r = {py = py ; ref_nb = int_of_string (Bson.get_jscode element) ; released = false} in 183 | Gc.finalise release_reference r ; 184 | Pyref (Pyreference r) 185 | | "n" -> Pynone 186 | | "e" -> raise (Pyexception "check the file python_log for traceback") 187 | | n -> raise (Unknown_return_type n) 188 | 189 | and py_call_raw py arg_to_not_finalize_ref release_ref set_attr modul dereference get_attr ret_ref mod_or_ref_bytes func_name args = 190 | let mod_or_ref_bytes = if dereference = false then mod_or_ref_bytes else ( 191 | match arg_to_not_finalize_ref with 192 | | Pymodule (py, name) -> raise Expected_reference_not_module 193 | | Pyreference {py ; ref_nb ; released} -> Bson.create_int64 (Int64.of_int ref_nb) 194 | ) in 195 | let doc = Bson.empty in 196 | let lst = serialize_list args in 197 | let doc = Bson.add_element "a" lst doc in 198 | let doc = Bson.add_element (if dereference then "g" else "f") (Bson.create_string func_name) doc in 199 | let doc = if get_attr then (Bson.add_element "t" (Bson.create_string "") doc) else doc in 200 | let doc = if set_attr then (Bson.add_element "s" (Bson.create_string "") doc) else doc in 201 | let doc = if ret_ref then (Bson.add_element "R" (Bson.create_string "") doc) else doc in 202 | let doc = if release_ref then (Bson.add_element "d" (Bson.create_string "") doc) else doc in 203 | (* mutex lock before making bytes, to prevent release of current reference *) 204 | let continue = ( if release_ref 205 | then 206 | Mutex.try_lock py.mutex 207 | else 208 | ( Mutex.lock py.mutex ; true ) 209 | ) in 210 | if continue = false then Pynone else ( 211 | let mod_or_ref_bytes = if dereference = false then mod_or_ref_bytes else ( 212 | match arg_to_not_finalize_ref with 213 | | Pymodule (py, name) -> raise Expected_reference_not_module 214 | | Pyreference {py ; ref_nb ; released} -> Bson.create_int64 (Int64.of_int ref_nb) 215 | ) in 216 | let doc = Bson.add_element (if modul then "m" else "r") mod_or_ref_bytes doc in 217 | let bytes = Bson.encode doc |> Bytes.of_string in 218 | send_raw_bytes py bytes ; 219 | let ret_bytes = get_raw_bytes py in 220 | Mutex.unlock py.mutex ; 221 | let ret_doc = Bson.decode (Bytes.to_string ret_bytes) in 222 | let ret_obj = deserialize py ret_doc in 223 | ret_obj 224 | ) 225 | 226 | and release_reference reference = 227 | match reference with 228 | | {py ; ref_nb ; released} -> 229 | ( if py.closed = false && released = false then 230 | ( reference.released <- true ; 231 | try ignore (py_call_raw py (Pymodule (py, "")) true false false false false false (Bson.create_int64 (Int64.of_int ref_nb)) "" []) with 232 | | _ -> () 233 | ) 234 | else 235 | () 236 | ) 237 | 238 | 239 | (* INTERFACE *) 240 | 241 | let get_module py mod_name = 242 | Pymodule (py, mod_name) 243 | 244 | let builtins py = 245 | Pymodule (py, "builtins") 246 | 247 | let get callable func args = 248 | match callable with 249 | | Pymodule (py, name) -> py_call_raw py callable false false true false false false (Bson.create_string name) func args 250 | | Pyreference {py ; ref_nb} -> py_call_raw py callable false false false false false false (Bson.create_int64 (Int64.of_int ref_nb)) func args 251 | 252 | let call callable func args = 253 | ignore (get callable func args) 254 | 255 | let get_string callable func args = 256 | match get callable func args with 257 | | Pystr s -> s 258 | | ret -> raise (make_wrongtype callable func "str" ret) 259 | 260 | let get_int callable func args = 261 | match get callable func args with 262 | | Pyint i -> i 263 | | ret -> raise (make_wrongtype callable func "int" ret) 264 | 265 | let get_float callable func args = 266 | match get callable func args with 267 | | Pyfloat f -> f 268 | | ret -> raise (make_wrongtype callable func "float" ret) 269 | 270 | let get_bool callable func args = 271 | match get callable func args with 272 | | Pybool b -> b 273 | | ret -> raise (make_wrongtype callable func "bool" ret) 274 | 275 | let get_bytes callable func args = 276 | match get callable func args with 277 | | Pybytes b -> b 278 | | ret -> raise (make_wrongtype callable func "bytes" ret) 279 | 280 | let get_ref callable func args = 281 | let ret = ( 282 | match callable with 283 | | Pymodule (py, name) -> py_call_raw py callable false false true false false true (Bson.create_string name) func args 284 | | Pyreference {py ; ref_nb ; released} -> py_call_raw py callable false false false false false true (Bson.create_int64 (Int64.of_int ref_nb)) func args 285 | ) in 286 | match ret with 287 | | Pyref r -> r 288 | | ret -> raise (make_wrongtype callable func "Pyref" ret) 289 | 290 | let get_tuple callable func args = 291 | match get callable func args with 292 | | Pytuple t -> t 293 | | ret -> raise (make_wrongtype callable func "tuple" ret) 294 | 295 | let get_list callable func args = 296 | match get callable func args with 297 | | Pylist l -> l 298 | | ret -> raise (make_wrongtype callable func "list" ret) 299 | 300 | let attr callable name = 301 | match callable with 302 | | Pymodule (py, name) -> py_call_raw py callable false false true false true false (Bson.create_string name) name [] 303 | | Pyreference {py ; ref_nb ; released} -> py_call_raw py callable false false false false true false (Bson.create_int64 (Int64.of_int ref_nb)) name [] 304 | 305 | let attr_string callable name = 306 | match attr callable name with 307 | | Pystr s -> s 308 | | ret -> raise (make_wrongtype callable name "str" ret) 309 | 310 | let attr_int callable name = 311 | match attr callable name with 312 | | Pyint i -> i 313 | | ret -> raise (make_wrongtype callable name "int" ret) 314 | 315 | let attr_float callable name = 316 | match attr callable name with 317 | | Pyfloat f -> f 318 | | ret -> raise (make_wrongtype callable name "float" ret) 319 | 320 | let attr_bool callable name = 321 | match attr callable name with 322 | | Pybool b -> b 323 | | ret -> raise (make_wrongtype callable name "bool" ret) 324 | 325 | let attr_bytes callable name = 326 | match attr callable name with 327 | | Pybytes b -> b 328 | | ret -> raise (make_wrongtype callable name "bytes" ret) 329 | 330 | let attr_ref callable name = 331 | let ret = ( 332 | match callable with 333 | | Pymodule (py, name) -> py_call_raw py callable false false true false true true (Bson.create_string name) name [] 334 | | Pyreference {py ; ref_nb ; released} -> py_call_raw py callable false false false false true true (Bson.create_int64 (Int64.of_int ref_nb)) name [] 335 | ) in 336 | match ret with 337 | | Pyref r -> r 338 | | ret -> raise (make_wrongtype callable name "Pyref" ret) 339 | 340 | let attr_tuple callable name = 341 | match attr callable name with 342 | | Pytuple t -> t 343 | | ret -> raise (make_wrongtype callable name "tuple" ret) 344 | 345 | let attr_list callable name = 346 | match attr callable name with 347 | | Pylist l -> l 348 | | ret -> raise (make_wrongtype callable name "list" ret) 349 | 350 | let set_attr callable name value = 351 | ignore ( match callable with 352 | | Pymodule (py, name) -> py_call_raw py callable false true true false false false (Bson.create_string name) name [value] 353 | | Pyreference {py ; ref_nb ; released} -> py_call_raw py callable false true false false false false (Bson.create_int64 (Int64.of_int ref_nb)) name [value] ) ; 354 | () 355 | 356 | let dereference r = 357 | match r with 358 | | Pymodule (py, name) -> raise Expected_reference_not_module 359 | | Pyreference {py ; ref_nb ; released} -> py_call_raw py r false false false true false false (Bson.create_string "") "" [] 360 | 361 | let close py = 362 | py.closed <- true ; 363 | Mutex.lock py.mutex ; 364 | send_raw_bytes py (Bytes.of_string "done") ; 365 | Mutex.unlock py.mutex ; 366 | ignore (Unix.close_process (py.process_in, py.process_out)) ; 367 | Sys.remove py.read_pipe.path ; 368 | Sys.remove py.write_pipe.path 369 | 370 | (* set ocamlfind to false if your ocamlfind is unable to find the package, 371 | lymp.py will be assumed to be in lymppy_dirpath *) 372 | let init ?(exec="python3") ?(ocamlfind=true) ?(lymppy_dirpath=".") pyroot = 373 | Random.self_init () ; 374 | let read_pipe_name = ".lymp_to_ocaml" in 375 | let write_pipe_name = ".lymp_to_python" in 376 | let read_pipe_path = pyroot ^ Filename.dir_sep ^ read_pipe_name in 377 | let write_pipe_path = pyroot ^ Filename.dir_sep ^ write_pipe_name in 378 | let read_pipe_randname = create_pipe read_pipe_path read_pipe_name in 379 | let write_pipe_randname = create_pipe write_pipe_path write_pipe_name in 380 | let process_in, process_out = create_process exec pyroot ocamlfind lymppy_dirpath read_pipe_randname write_pipe_randname in 381 | let read_pipe, write_pipe = get_pipes (pyroot ^ Filename.dir_sep ^ read_pipe_randname) (pyroot ^ Filename.dir_sep ^ write_pipe_randname) in 382 | { 383 | read_pipe = read_pipe ; 384 | write_pipe = write_pipe ; 385 | process_in = process_in ; 386 | process_out = process_out ; 387 | mutex = Mutex.create () ; 388 | closed = false 389 | } 390 | -------------------------------------------------------------------------------- /srcs/bson.ml: -------------------------------------------------------------------------------- 1 | exception Invalid_objectId;; 2 | exception Wrong_bson_type;; 3 | exception Wrong_string;; 4 | exception Malformed_bson;; 5 | 6 | type document = (string * element) list 7 | and t = document 8 | and special = 9 | | NULL 10 | | MINKEY 11 | | MAXKEY 12 | and element = 13 | | Double of float 14 | | String of string 15 | | Document of document 16 | | Array of element list 17 | | Binary of binary 18 | | ObjectId of string (* only 12 bytes *) 19 | | Boolean of bool 20 | | UTC of int64 21 | | Null of special 22 | | Regex of (string * string) 23 | | JSCode of string 24 | | JSCodeWS of (string * document) 25 | | Int32 of int32 26 | | Int64 of int64 27 | | Timestamp of int64 28 | | MinKey of special 29 | | MaxKey of special 30 | and binary = 31 | | Generic of string 32 | | Function of string 33 | | UUID of string 34 | | MD5 of string 35 | | UserDefined of string;; 36 | 37 | 38 | let empty = [];; 39 | 40 | let is_empty = function 41 | | [] -> true 42 | | _ -> false;; 43 | 44 | let has_element = List.mem_assoc 45 | 46 | (* 47 | The remove operations. 48 | *) 49 | let remove_element = List.remove_assoc 50 | 51 | (* 52 | for constructing a document 53 | 1. we make a empty document 54 | 2. we create element as we want 55 | 3. we add the element to the document, with a element name 56 | *) 57 | let add_element ename element doc = 58 | (* Emulating StringMap add operation *) 59 | let doc = 60 | if has_element ename doc then remove_element ename doc 61 | else doc 62 | in 63 | (ename,element)::doc;; 64 | 65 | (* 66 | for using a document 67 | 1. we get an element from document, if existing 68 | 2. we get the value of the element 69 | *) 70 | let get_element = List.assoc 71 | 72 | 73 | 74 | let create_double v = Double v;; 75 | let create_string v = String v;; 76 | let create_doc_element v = Document v;; 77 | let create_list l = Array l;; 78 | let create_doc_element_list l = create_list (List.map create_doc_element l);; 79 | let create_generic_binary v = Binary (Generic v);; 80 | let create_function_binary v = Binary (Function v);; 81 | let create_uuid_binary v = Binary (UUID v);; 82 | let create_md5_binary v = Binary (MD5 v);; 83 | let create_user_binary v = Binary (UserDefined v);; 84 | let is_valid_objectId objectId = if String.length objectId = 12 || String.length objectId = 24 then true else false;; 85 | let hex_to_string s = 86 | let n = String.length s in 87 | let buf = Buffer.create 12 in 88 | let rec convert i = 89 | if i > n-1 then Buffer.contents buf 90 | else begin 91 | Buffer.add_char buf (char_of_int (int_of_string ("0x" ^ (String.sub s i 2)))); 92 | convert (i+2) 93 | end 94 | in 95 | convert 0 96 | let create_objectId v = 97 | if String.length v = 12 then ObjectId v 98 | else if String.length v = 24 then 99 | try (ObjectId (hex_to_string v)) with (Failure "int_of_string") -> raise Invalid_objectId 100 | else raise Invalid_objectId;; 101 | let create_boolean v = Boolean v;; 102 | let create_utc v = UTC v;; 103 | let create_null () = Null NULL;; 104 | let create_regex s1 s2 = Regex (s1, s2);; 105 | let create_jscode v = JSCode v;; 106 | let create_jscode_w_s s doc = JSCodeWS (s, doc);; 107 | let create_int32 v = Int32 v;; 108 | let create_int64 v = Int64 v;; 109 | let create_timestamp v = Timestamp v;; 110 | let create_minkey () = MinKey MINKEY;; 111 | let create_maxkey () = MaxKey MAXKEY;; 112 | 113 | let get_double = function | Double v -> v | _ -> raise Wrong_bson_type;; 114 | let get_string = function | String v -> v | _ -> raise Wrong_bson_type;; 115 | let get_doc_element = function | Document v -> v | _ -> raise Wrong_bson_type;; 116 | let get_list = function | Array v -> v | _ -> raise Wrong_bson_type;; 117 | let get_generic_binary = function | Binary (Generic v) -> v | _ -> raise Wrong_bson_type;; 118 | let get_function_binary = function | Binary (Function v) -> v | _ -> raise Wrong_bson_type;; 119 | let get_uuid_binary = function | Binary (UUID v) -> v | _ -> raise Wrong_bson_type;; 120 | let get_md5_binary = function | Binary (MD5 v) -> v | _ -> raise Wrong_bson_type;; 121 | let get_user_binary = function | Binary (UserDefined v) -> v | _ -> raise Wrong_bson_type;; 122 | let get_objectId = function | ObjectId v -> v | _ -> raise Wrong_bson_type;; 123 | let get_boolean = function | Boolean v -> v | _ -> raise Wrong_bson_type;; 124 | let get_utc = function | UTC v -> v | _ -> raise Wrong_bson_type;; 125 | let get_null = function | Null NULL -> NULL | _ -> raise Wrong_bson_type;; 126 | let get_regex = function | Regex v -> v | _ -> raise Wrong_bson_type;; 127 | let get_jscode = function | JSCode v -> v | _ -> raise Wrong_bson_type;; 128 | let get_jscode_w_s = function | JSCodeWS v -> v | _ -> raise Wrong_bson_type;; 129 | let get_int32 = function | Int32 v -> v | _ -> raise Wrong_bson_type;; 130 | let get_int64 = function | Int64 v -> v | _ -> raise Wrong_bson_type;; 131 | let get_timestamp = function | Timestamp v -> v | _ -> raise Wrong_bson_type;; 132 | let get_minkey = function | MinKey MINKEY -> MINKEY | _ -> raise Wrong_bson_type;; 133 | let get_maxkey = function | MaxKey MAXKEY -> MAXKEY | _ -> raise Wrong_bson_type;; 134 | 135 | let all_elements d = d 136 | 137 | (* 138 | encode int64, int32 and float. 139 | note that encoding float is the same as int64, just need to transfer all the bits into an int64. 140 | 141 | The logic is that (e.g., for int32): 142 | 1) we get an int32 143 | 2) we shift right 1 byte one by one 144 | 3) After each shift, we logic and 0000 0000 ... 0000 1111 1111 (255l) with the shifted int32 to get the lower 1 byte 145 | 4) we convert the int32 to int, so Char.chr can pick it up and convert it to char (byte) 146 | 5) we put the byte to the buffer (starting from index of 0, since it is little-endian format) 147 | *) 148 | 149 | let encode_int64 buf v = 150 | for i = 0 to 7 do 151 | let b = Int64.logand 255L (Int64.shift_right v (i*8)) in 152 | Buffer.add_char buf (Char.chr (Int64.to_int b)) 153 | done;; 154 | 155 | let encode_float buf v = encode_int64 buf (Int64.bits_of_float v);; 156 | 157 | let encode_int32 buf v = 158 | for i = 0 to 3 do 159 | let b = Int32.logand 255l (Int32.shift_right v (i*8)) in 160 | Buffer.add_char buf (Char.chr (Int32.to_int b)) 161 | done;; 162 | 163 | let encode_ename buf c ename = 164 | Buffer.add_char buf c; 165 | Buffer.add_string buf ename; 166 | Buffer.add_char buf '\x00';; 167 | 168 | let encode_string buf s = 169 | let len = String.length s in 170 | if len > 0 && s.[len-1] = '\x00' then raise Wrong_string 171 | else begin 172 | encode_int32 buf (Int32.of_int (len+1)); 173 | Buffer.add_string buf s; 174 | Buffer.add_char buf '\x00' 175 | end;; 176 | 177 | let encode_objectId buf s = 178 | if String.length s <> 12 then raise Invalid_objectId 179 | else Buffer.add_string buf s;; 180 | 181 | let encode_binary buf c b = 182 | encode_int32 buf (Int32.of_int (String.length b)); 183 | Buffer.add_char buf c; 184 | Buffer.add_string buf b;; 185 | 186 | let encode_cstring buf cs = 187 | Buffer.add_string buf cs; 188 | Buffer.add_char buf '\x00';; 189 | 190 | let list_to_doc l = (* we need to transform the list to a doc with key as incrementing from '0' *) 191 | let rec to_doc i acc = function 192 | | [] -> acc 193 | | hd::tl -> to_doc (i+1) (add_element (string_of_int i) hd acc) tl 194 | in 195 | to_doc 0 empty l;; 196 | 197 | 198 | let encode doc = 199 | let all_buf = Buffer.create 64 in 200 | let rec encode_element buf ename element = 201 | match element with 202 | | Double v -> 203 | encode_ename buf '\x01' ename; 204 | encode_float buf v 205 | | String v -> 206 | encode_ename buf '\x02' ename; 207 | encode_string buf v 208 | | Document v -> 209 | encode_ename buf '\x03' ename; 210 | encode_doc buf v 211 | | Array v -> 212 | encode_ename buf '\x04' ename; 213 | encode_doc buf (list_to_doc v) 214 | | Binary v -> 215 | encode_ename buf '\x05' ename; 216 | begin match v with 217 | | Generic v -> encode_binary buf '\x00' v 218 | | Function v -> encode_binary buf '\x01' v 219 | | UUID v -> encode_binary buf '\x04' v 220 | | MD5 v -> encode_binary buf '\x05' v 221 | | UserDefined v -> encode_binary buf '\x80' v 222 | end 223 | | ObjectId v -> 224 | encode_ename buf '\x07' ename; 225 | encode_objectId buf v 226 | | Boolean v -> 227 | encode_ename buf '\x08' ename; 228 | Buffer.add_char buf (if v then '\x01' else '\x00') 229 | | UTC v -> 230 | encode_ename buf '\x09' ename; 231 | encode_int64 buf v 232 | | Null NULL-> 233 | encode_ename buf '\x0A' ename; 234 | | Regex (v1,v2) -> 235 | encode_ename buf '\x0B' ename; 236 | encode_cstring buf v1; 237 | encode_cstring buf v2 238 | | JSCode v -> 239 | encode_ename buf '\x0D' ename; 240 | encode_string buf v 241 | | JSCodeWS (v, d) -> 242 | encode_ename buf '\x0F' ename; 243 | let tmp_str_buf = Buffer.create 16 and tmp_doc_buf = Buffer.create 16 in 244 | encode_string tmp_str_buf v; 245 | encode_doc tmp_doc_buf d; 246 | encode_int32 buf (Int32.of_int (4 + (Buffer.length tmp_str_buf) + (Buffer.length tmp_doc_buf))); 247 | Buffer.add_buffer buf tmp_str_buf; 248 | Buffer.add_buffer buf tmp_doc_buf 249 | | Int32 v -> 250 | encode_ename buf '\x10' ename; 251 | encode_int32 buf v 252 | | Timestamp v -> 253 | encode_ename buf '\x11' ename; 254 | encode_int64 buf v 255 | | Int64 v -> 256 | encode_ename buf '\x12' ename; 257 | encode_int64 buf v 258 | | MinKey MINKEY -> 259 | encode_ename buf '\xFF' ename 260 | | MaxKey MAXKEY -> 261 | encode_ename buf '\x7F' ename 262 | | _ -> raise Malformed_bson 263 | and 264 | encode_doc buf doc = 265 | let process_element buf (ename, element) = encode_element buf ename element; buf in 266 | let e_buf = List.fold_left process_element (Buffer.create 64) doc in 267 | encode_int32 buf (Int32.of_int (5+(Buffer.length e_buf))); 268 | Buffer.add_buffer buf e_buf; 269 | Buffer.add_char buf '\x00'; 270 | in 271 | encode_doc all_buf doc; 272 | Buffer.contents all_buf;; 273 | 274 | 275 | let decode_int64 str cur = 276 | let rec decode i acc = 277 | if i < cur then acc 278 | else 279 | let high_byte = Char.code str.[i] in 280 | let high_int64 = Int64.of_int high_byte in 281 | let shift_acc = Int64.shift_left acc 8 in 282 | let new_acc = Int64.logor high_int64 shift_acc in 283 | decode (i-1) new_acc 284 | in (decode (cur+7) 0L, cur+8) 285 | 286 | let decode_float str cur = 287 | let (i, new_cur) = decode_int64 str cur in 288 | (Int64.float_of_bits i, new_cur);; 289 | 290 | let decode_int32 str cur = 291 | let rec decode i acc = 292 | if i < cur then acc 293 | else 294 | let high_byte = Char.code str.[i] in 295 | (*print_int high_byte;print_endline "";*) 296 | let high_int32 = Int32.of_int high_byte in 297 | let shift_acc = Int32.shift_left acc 8 in 298 | let new_acc = Int32.logor high_int32 shift_acc in 299 | decode (i-1) new_acc 300 | in (decode (cur+3) 0l, cur+4);; 301 | 302 | let rec next_x00 str cur = String.index_from str cur '\x00';; 303 | 304 | let decode_ename str cur = 305 | let x00 = next_x00 str cur in 306 | if x00 = -1 then raise Malformed_bson 307 | else (String.sub str cur (x00-cur), x00+1);; 308 | 309 | let decode_cstring = decode_ename;; 310 | 311 | let decode_len str cur = 312 | let (len32, next_cur) = decode_int32 str cur in 313 | (Int32.to_int len32, next_cur) 314 | 315 | let decode_double str cur = 316 | let (f, new_cur) = decode_float str cur in 317 | (Double f, new_cur);; 318 | 319 | let decode_string str cur = 320 | let (len, next_cur) = decode_len str cur in 321 | (*print_string "cur=";print_int cur;print_string ";"; 322 | print_string "len=";print_int len; 323 | print_endline "";*) 324 | (*let x00 = next_x00 str next_cur in 325 | Printf.printf "len=%d, next_cur=%d, x00=%d, s[x00]=%c\n" len next_cur x00 str.[x00-1]; 326 | print_endline (String.sub str next_cur (len-1));*) 327 | (*if len - 1 <> x00-next_cur then raise Wrong_string 328 | else (String.sub str next_cur (len-1), x00+1);;*) 329 | (String.sub str next_cur (len-1), next_cur+len);; 330 | 331 | let doc_to_list doc = (* we need to transform a doc with key as incrementing from '0' to a list *) 332 | List.map ( 333 | fun (k,v) -> v 334 | ) doc 335 | 336 | 337 | let decode_binary str cur = 338 | let (len, next_cur) = decode_len str cur in 339 | let c = str.[next_cur] in 340 | let b = String.sub str (next_cur+1) len in 341 | let new_cur = next_cur+1+len in 342 | match c with 343 | | '\x00' -> (Binary (Generic b), new_cur) 344 | | '\x01' -> (Binary (Function b), new_cur) 345 | | '\x04' -> (Binary (UUID b), new_cur) 346 | | '\x05' -> (Binary (MD5 b), new_cur) 347 | | '\x80' -> (Binary (UserDefined b), new_cur) 348 | | _ -> raise Malformed_bson;; 349 | 350 | let decode_objectId str cur = (ObjectId (String.sub str cur 12), cur+12);; 351 | 352 | let decode_boolean str cur = (Boolean (if str.[cur] = '\x00' then false else true), cur+1);; 353 | 354 | let decode_utc str cur = 355 | let (i, new_cur) = decode_int64 str cur in 356 | (UTC i, new_cur);; 357 | 358 | let decode_regex str cur = 359 | let (s1, x00) = decode_cstring str cur in 360 | let (s2, new_cur) = decode_cstring str (x00+1) in 361 | (Regex (s1, s2), new_cur);; 362 | 363 | let decode_jscode str cur = 364 | let (s, next_cur) = decode_string str cur in 365 | (JSCode s, next_cur);; 366 | 367 | let decode str = 368 | let rec decode_element str cur = 369 | let c = str.[cur] in 370 | let next_cur = cur+1 in 371 | let (ename, next_cur) = decode_ename str next_cur in 372 | (*print_endline ename;*) 373 | let (element, next_cur) = 374 | match c with 375 | | '\x01' -> decode_double str next_cur 376 | | '\x02' -> 377 | (*print_endline "decoding string...";*) 378 | let (s, next_cur) = decode_string str next_cur in 379 | (String s, next_cur) 380 | | '\x03' -> 381 | let (doc, next_cur) = decode_doc str next_cur in 382 | (Document doc, next_cur) 383 | | '\x04' -> 384 | let (doc, next_cur) = decode_doc str next_cur in 385 | (Array (doc_to_list doc), next_cur) 386 | | '\x05' -> decode_binary str next_cur 387 | | '\x07' -> decode_objectId str next_cur 388 | | '\x08' -> decode_boolean str next_cur 389 | | '\x09' -> decode_utc str next_cur 390 | | '\x0A' -> (Null NULL, next_cur) 391 | | '\x0B' -> decode_regex str next_cur 392 | | '\x0D' -> decode_jscode str next_cur 393 | | '\x0F' -> (* decode jscode_w_s *) 394 | let (len, next_cur) = decode_len str next_cur in 395 | let (s, next_cur) = decode_string str next_cur in 396 | let (doc, next_cur) = decode_doc str next_cur in 397 | (JSCodeWS (s, doc), next_cur) 398 | | '\x10' -> 399 | let (i, next_cur) = decode_int32 str next_cur in 400 | (Int32 i, next_cur) 401 | | '\x11' -> 402 | let (i, next_cur) = decode_int64 str next_cur in 403 | (Timestamp i, next_cur) 404 | | '\x12' -> 405 | let (i, next_cur) = decode_int64 str next_cur in 406 | (Int64 i, next_cur) 407 | | '\xFF' -> (MinKey MINKEY, next_cur) 408 | | '\x7F' -> (MaxKey MAXKEY, next_cur) 409 | | _ -> raise Malformed_bson 410 | in 411 | (ename, element, next_cur) 412 | and decode_doc str cur = 413 | let acc = empty in 414 | let (len, next_cur) = decode_len str cur in 415 | let rec decode_elements cur acc = 416 | if str.[cur] = '\x00' then (acc, cur+1) 417 | else 418 | let (ename, element, next_cur) = decode_element str cur in 419 | decode_elements next_cur (add_element ename element acc) 420 | in 421 | let (doc, des) = decode_elements next_cur acc in 422 | if des - cur <> len then raise Malformed_bson 423 | else (doc, des) 424 | in let (doc, _) = decode_doc str 0 in doc;; 425 | 426 | (* 427 | Not that this bson to json conversion is far from completion. 428 | It is used to help the test verification and can handle only simple objects. 429 | *) 430 | let to_simple_json doc = 431 | let rec el_to_sl el = 432 | List.rev (List.fold_left (fun acc e -> (e_to_s e)::acc) [] el) 433 | and e_to_s = function 434 | | Double v -> string_of_float v 435 | | String v -> "\"" ^ v ^ "\"" 436 | | Document v -> d_to_s v 437 | | Array v -> let sl = el_to_sl v in "[" ^ (String.concat ", " sl) ^ "]" 438 | | Binary v -> 439 | begin match v with 440 | | Generic v | Function v | UUID v | MD5 v | UserDefined v -> "\"" ^ v ^ "\"" 441 | end 442 | | ObjectId v -> "\"" ^ v ^ "\"" 443 | | Boolean v -> if v then "\"true\"" else "\"false\"" 444 | | UTC v -> Int64.to_string v 445 | | Null NULL-> "\"null\"" 446 | | Regex (v1,v2) -> "(\"" ^ v1 ^ ", \"" ^ v2 ^ "\")" 447 | | JSCode v -> "\"" ^ v ^ "\"" 448 | | JSCodeWS (v, d) -> "(\"" ^ v ^ ", \"" ^ (d_to_s d) ^ "\")" 449 | | Int32 v -> Int32.to_string v 450 | | Timestamp v -> Int64.to_string v 451 | | Int64 v -> Int64.to_string v 452 | | MinKey MINKEY -> "\"minkey\"" 453 | | MaxKey MAXKEY -> "\"maxkey\"" 454 | | _ -> raise Malformed_bson 455 | and d_to_s d = 456 | let buf = Buffer.create 16 in 457 | Buffer.add_string buf "{"; 458 | (* let bindings = all_elements d in *) 459 | let process acc (ename, element) = 460 | ("\"" ^ ename ^ "\" : " ^ (e_to_s element)) :: acc; 461 | in 462 | Buffer.add_string buf (String.concat ", " (List.rev (List.fold_left process [] d))); 463 | Buffer.add_string buf "}"; 464 | Buffer.contents buf 465 | in 466 | d_to_s doc;; 467 | -------------------------------------------------------------------------------- /myocamlbuild.ml: -------------------------------------------------------------------------------- 1 | (* OASIS_START *) 2 | (* DO NOT EDIT (digest: 7c79194eb16194bf2a2a530cc49aff34) *) 3 | module OASISGettext = struct 4 | (* # 22 "src/oasis/OASISGettext.ml" *) 5 | 6 | 7 | let ns_ str = str 8 | let s_ str = str 9 | let f_ (str: ('a, 'b, 'c, 'd) format4) = str 10 | 11 | 12 | let fn_ fmt1 fmt2 n = 13 | if n = 1 then 14 | fmt1^^"" 15 | else 16 | fmt2^^"" 17 | 18 | 19 | let init = [] 20 | end 21 | 22 | module OASISString = struct 23 | (* # 22 "src/oasis/OASISString.ml" *) 24 | 25 | 26 | (** Various string utilities. 27 | 28 | Mostly inspired by extlib and batteries ExtString and BatString libraries. 29 | 30 | @author Sylvain Le Gall 31 | *) 32 | 33 | 34 | let nsplitf str f = 35 | if str = "" then 36 | [] 37 | else 38 | let buf = Buffer.create 13 in 39 | let lst = ref [] in 40 | let push () = 41 | lst := Buffer.contents buf :: !lst; 42 | Buffer.clear buf 43 | in 44 | let str_len = String.length str in 45 | for i = 0 to str_len - 1 do 46 | if f str.[i] then 47 | push () 48 | else 49 | Buffer.add_char buf str.[i] 50 | done; 51 | push (); 52 | List.rev !lst 53 | 54 | 55 | (** [nsplit c s] Split the string [s] at char [c]. It doesn't include the 56 | separator. 57 | *) 58 | let nsplit str c = 59 | nsplitf str ((=) c) 60 | 61 | 62 | let find ~what ?(offset=0) str = 63 | let what_idx = ref 0 in 64 | let str_idx = ref offset in 65 | while !str_idx < String.length str && 66 | !what_idx < String.length what do 67 | if str.[!str_idx] = what.[!what_idx] then 68 | incr what_idx 69 | else 70 | what_idx := 0; 71 | incr str_idx 72 | done; 73 | if !what_idx <> String.length what then 74 | raise Not_found 75 | else 76 | !str_idx - !what_idx 77 | 78 | 79 | let sub_start str len = 80 | let str_len = String.length str in 81 | if len >= str_len then 82 | "" 83 | else 84 | String.sub str len (str_len - len) 85 | 86 | 87 | let sub_end ?(offset=0) str len = 88 | let str_len = String.length str in 89 | if len >= str_len then 90 | "" 91 | else 92 | String.sub str 0 (str_len - len) 93 | 94 | 95 | let starts_with ~what ?(offset=0) str = 96 | let what_idx = ref 0 in 97 | let str_idx = ref offset in 98 | let ok = ref true in 99 | while !ok && 100 | !str_idx < String.length str && 101 | !what_idx < String.length what do 102 | if str.[!str_idx] = what.[!what_idx] then 103 | incr what_idx 104 | else 105 | ok := false; 106 | incr str_idx 107 | done; 108 | !what_idx = String.length what 109 | 110 | 111 | let strip_starts_with ~what str = 112 | if starts_with ~what str then 113 | sub_start str (String.length what) 114 | else 115 | raise Not_found 116 | 117 | 118 | let ends_with ~what ?(offset=0) str = 119 | let what_idx = ref ((String.length what) - 1) in 120 | let str_idx = ref ((String.length str) - 1) in 121 | let ok = ref true in 122 | while !ok && 123 | offset <= !str_idx && 124 | 0 <= !what_idx do 125 | if str.[!str_idx] = what.[!what_idx] then 126 | decr what_idx 127 | else 128 | ok := false; 129 | decr str_idx 130 | done; 131 | !what_idx = -1 132 | 133 | 134 | let strip_ends_with ~what str = 135 | if ends_with ~what str then 136 | sub_end str (String.length what) 137 | else 138 | raise Not_found 139 | 140 | 141 | let replace_chars f s = 142 | let buf = Buffer.create (String.length s) in 143 | String.iter (fun c -> Buffer.add_char buf (f c)) s; 144 | Buffer.contents buf 145 | 146 | let lowercase_ascii = 147 | replace_chars 148 | (fun c -> 149 | if (c >= 'A' && c <= 'Z') then 150 | Char.chr (Char.code c + 32) 151 | else 152 | c) 153 | 154 | let uncapitalize_ascii s = 155 | if s <> "" then 156 | (lowercase_ascii (String.sub s 0 1)) ^ (String.sub s 1 ((String.length s) - 1)) 157 | else 158 | s 159 | 160 | let uppercase_ascii = 161 | replace_chars 162 | (fun c -> 163 | if (c >= 'a' && c <= 'z') then 164 | Char.chr (Char.code c - 32) 165 | else 166 | c) 167 | 168 | let capitalize_ascii s = 169 | if s <> "" then 170 | (uppercase_ascii (String.sub s 0 1)) ^ (String.sub s 1 ((String.length s) - 1)) 171 | else 172 | s 173 | 174 | end 175 | 176 | module OASISUtils = struct 177 | (* # 22 "src/oasis/OASISUtils.ml" *) 178 | 179 | 180 | open OASISGettext 181 | 182 | 183 | module MapExt = 184 | struct 185 | module type S = 186 | sig 187 | include Map.S 188 | val add_list: 'a t -> (key * 'a) list -> 'a t 189 | val of_list: (key * 'a) list -> 'a t 190 | val to_list: 'a t -> (key * 'a) list 191 | end 192 | 193 | module Make (Ord: Map.OrderedType) = 194 | struct 195 | include Map.Make(Ord) 196 | 197 | let rec add_list t = 198 | function 199 | | (k, v) :: tl -> add_list (add k v t) tl 200 | | [] -> t 201 | 202 | let of_list lst = add_list empty lst 203 | 204 | let to_list t = fold (fun k v acc -> (k, v) :: acc) t [] 205 | end 206 | end 207 | 208 | 209 | module MapString = MapExt.Make(String) 210 | 211 | 212 | module SetExt = 213 | struct 214 | module type S = 215 | sig 216 | include Set.S 217 | val add_list: t -> elt list -> t 218 | val of_list: elt list -> t 219 | val to_list: t -> elt list 220 | end 221 | 222 | module Make (Ord: Set.OrderedType) = 223 | struct 224 | include Set.Make(Ord) 225 | 226 | let rec add_list t = 227 | function 228 | | e :: tl -> add_list (add e t) tl 229 | | [] -> t 230 | 231 | let of_list lst = add_list empty lst 232 | 233 | let to_list = elements 234 | end 235 | end 236 | 237 | 238 | module SetString = SetExt.Make(String) 239 | 240 | 241 | let compare_csl s1 s2 = 242 | String.compare (OASISString.lowercase_ascii s1) (OASISString.lowercase_ascii s2) 243 | 244 | 245 | module HashStringCsl = 246 | Hashtbl.Make 247 | (struct 248 | type t = string 249 | let equal s1 s2 = (compare_csl s1 s2) = 0 250 | let hash s = Hashtbl.hash (OASISString.lowercase_ascii s) 251 | end) 252 | 253 | module SetStringCsl = 254 | SetExt.Make 255 | (struct 256 | type t = string 257 | let compare = compare_csl 258 | end) 259 | 260 | 261 | let varname_of_string ?(hyphen='_') s = 262 | if String.length s = 0 then 263 | begin 264 | invalid_arg "varname_of_string" 265 | end 266 | else 267 | begin 268 | let buf = 269 | OASISString.replace_chars 270 | (fun c -> 271 | if ('a' <= c && c <= 'z') 272 | || 273 | ('A' <= c && c <= 'Z') 274 | || 275 | ('0' <= c && c <= '9') then 276 | c 277 | else 278 | hyphen) 279 | s; 280 | in 281 | let buf = 282 | (* Start with a _ if digit *) 283 | if '0' <= s.[0] && s.[0] <= '9' then 284 | "_"^buf 285 | else 286 | buf 287 | in 288 | OASISString.lowercase_ascii buf 289 | end 290 | 291 | 292 | let varname_concat ?(hyphen='_') p s = 293 | let what = String.make 1 hyphen in 294 | let p = 295 | try 296 | OASISString.strip_ends_with ~what p 297 | with Not_found -> 298 | p 299 | in 300 | let s = 301 | try 302 | OASISString.strip_starts_with ~what s 303 | with Not_found -> 304 | s 305 | in 306 | p^what^s 307 | 308 | 309 | let is_varname str = 310 | str = varname_of_string str 311 | 312 | 313 | let failwithf fmt = Printf.ksprintf failwith fmt 314 | 315 | 316 | let rec file_location ?pos1 ?pos2 ?lexbuf () = 317 | match pos1, pos2, lexbuf with 318 | | Some p, None, _ | None, Some p, _ -> 319 | file_location ~pos1:p ~pos2:p ?lexbuf () 320 | | Some p1, Some p2, _ -> 321 | let open Lexing in 322 | let fn, lineno = p1.pos_fname, p1.pos_lnum in 323 | let c1 = p1.pos_cnum - p1.pos_bol in 324 | let c2 = c1 + (p2.pos_cnum - p1.pos_cnum) in 325 | Printf.sprintf (f_ "file %S, line %d, characters %d-%d") fn lineno c1 c2 326 | | _, _, Some lexbuf -> 327 | file_location 328 | ~pos1:(Lexing.lexeme_start_p lexbuf) 329 | ~pos2:(Lexing.lexeme_end_p lexbuf) 330 | () 331 | | None, None, None -> 332 | s_ "" 333 | 334 | 335 | let failwithpf ?pos1 ?pos2 ?lexbuf fmt = 336 | let loc = file_location ?pos1 ?pos2 ?lexbuf () in 337 | Printf.ksprintf (fun s -> failwith (Printf.sprintf "%s: %s" loc s)) fmt 338 | 339 | 340 | end 341 | 342 | module OASISExpr = struct 343 | (* # 22 "src/oasis/OASISExpr.ml" *) 344 | 345 | 346 | open OASISGettext 347 | open OASISUtils 348 | 349 | 350 | type test = string 351 | type flag = string 352 | 353 | 354 | type t = 355 | | EBool of bool 356 | | ENot of t 357 | | EAnd of t * t 358 | | EOr of t * t 359 | | EFlag of flag 360 | | ETest of test * string 361 | 362 | 363 | type 'a choices = (t * 'a) list 364 | 365 | 366 | let eval var_get t = 367 | let rec eval' = 368 | function 369 | | EBool b -> 370 | b 371 | 372 | | ENot e -> 373 | not (eval' e) 374 | 375 | | EAnd (e1, e2) -> 376 | (eval' e1) && (eval' e2) 377 | 378 | | EOr (e1, e2) -> 379 | (eval' e1) || (eval' e2) 380 | 381 | | EFlag nm -> 382 | let v = 383 | var_get nm 384 | in 385 | assert(v = "true" || v = "false"); 386 | (v = "true") 387 | 388 | | ETest (nm, vl) -> 389 | let v = 390 | var_get nm 391 | in 392 | (v = vl) 393 | in 394 | eval' t 395 | 396 | 397 | let choose ?printer ?name var_get lst = 398 | let rec choose_aux = 399 | function 400 | | (cond, vl) :: tl -> 401 | if eval var_get cond then 402 | vl 403 | else 404 | choose_aux tl 405 | | [] -> 406 | let str_lst = 407 | if lst = [] then 408 | s_ "" 409 | else 410 | String.concat 411 | (s_ ", ") 412 | (List.map 413 | (fun (cond, vl) -> 414 | match printer with 415 | | Some p -> p vl 416 | | None -> s_ "") 417 | lst) 418 | in 419 | match name with 420 | | Some nm -> 421 | failwith 422 | (Printf.sprintf 423 | (f_ "No result for the choice list '%s': %s") 424 | nm str_lst) 425 | | None -> 426 | failwith 427 | (Printf.sprintf 428 | (f_ "No result for a choice list: %s") 429 | str_lst) 430 | in 431 | choose_aux (List.rev lst) 432 | 433 | 434 | end 435 | 436 | 437 | # 437 "myocamlbuild.ml" 438 | module BaseEnvLight = struct 439 | (* # 22 "src/base/BaseEnvLight.ml" *) 440 | 441 | 442 | module MapString = Map.Make(String) 443 | 444 | 445 | type t = string MapString.t 446 | 447 | 448 | let default_filename = Filename.concat (Sys.getcwd ()) "setup.data" 449 | 450 | 451 | let load ?(allow_empty=false) ?(filename=default_filename) ?stream () = 452 | let line = ref 1 in 453 | let lexer st = 454 | let st_line = 455 | Stream.from 456 | (fun _ -> 457 | try 458 | match Stream.next st with 459 | | '\n' -> incr line; Some '\n' 460 | | c -> Some c 461 | with Stream.Failure -> None) 462 | in 463 | Genlex.make_lexer ["="] st_line 464 | in 465 | let rec read_file lxr mp = 466 | match Stream.npeek 3 lxr with 467 | | [Genlex.Ident nm; Genlex.Kwd "="; Genlex.String value] -> 468 | Stream.junk lxr; Stream.junk lxr; Stream.junk lxr; 469 | read_file lxr (MapString.add nm value mp) 470 | | [] -> mp 471 | | _ -> 472 | failwith 473 | (Printf.sprintf "Malformed data file '%s' line %d" filename !line) 474 | in 475 | match stream with 476 | | Some st -> read_file (lexer st) MapString.empty 477 | | None -> 478 | if Sys.file_exists filename then begin 479 | let chn = open_in_bin filename in 480 | let st = Stream.of_channel chn in 481 | try 482 | let mp = read_file (lexer st) MapString.empty in 483 | close_in chn; mp 484 | with e -> 485 | close_in chn; raise e 486 | end else if allow_empty then begin 487 | MapString.empty 488 | end else begin 489 | failwith 490 | (Printf.sprintf 491 | "Unable to load environment, the file '%s' doesn't exist." 492 | filename) 493 | end 494 | 495 | let rec var_expand str env = 496 | let buff = Buffer.create ((String.length str) * 2) in 497 | Buffer.add_substitute 498 | buff 499 | (fun var -> 500 | try 501 | var_expand (MapString.find var env) env 502 | with Not_found -> 503 | failwith 504 | (Printf.sprintf 505 | "No variable %s defined when trying to expand %S." 506 | var 507 | str)) 508 | str; 509 | Buffer.contents buff 510 | 511 | 512 | let var_get name env = var_expand (MapString.find name env) env 513 | let var_choose lst env = OASISExpr.choose (fun nm -> var_get nm env) lst 514 | end 515 | 516 | 517 | # 517 "myocamlbuild.ml" 518 | module MyOCamlbuildFindlib = struct 519 | (* # 22 "src/plugins/ocamlbuild/MyOCamlbuildFindlib.ml" *) 520 | 521 | 522 | (** OCamlbuild extension, copied from 523 | * https://ocaml.org/learn/tutorials/ocamlbuild/Using_ocamlfind_with_ocamlbuild.html 524 | * by N. Pouillard and others 525 | * 526 | * Updated on 2016-06-02 527 | * 528 | * Modified by Sylvain Le Gall 529 | *) 530 | open Ocamlbuild_plugin 531 | 532 | 533 | type conf = {no_automatic_syntax: bool} 534 | 535 | 536 | let run_and_read = Ocamlbuild_pack.My_unix.run_and_read 537 | 538 | 539 | let blank_sep_strings = Ocamlbuild_pack.Lexers.blank_sep_strings 540 | 541 | 542 | let exec_from_conf exec = 543 | let exec = 544 | let env = BaseEnvLight.load ~allow_empty:true () in 545 | try 546 | BaseEnvLight.var_get exec env 547 | with Not_found -> 548 | Printf.eprintf "W: Cannot get variable %s\n" exec; 549 | exec 550 | in 551 | let fix_win32 str = 552 | if Sys.os_type = "Win32" then begin 553 | let buff = Buffer.create (String.length str) in 554 | (* Adapt for windowsi, ocamlbuild + win32 has a hard time to handle '\\'. 555 | *) 556 | String.iter 557 | (fun c -> Buffer.add_char buff (if c = '\\' then '/' else c)) 558 | str; 559 | Buffer.contents buff 560 | end else begin 561 | str 562 | end 563 | in 564 | fix_win32 exec 565 | 566 | 567 | let split s ch = 568 | let buf = Buffer.create 13 in 569 | let x = ref [] in 570 | let flush () = 571 | x := (Buffer.contents buf) :: !x; 572 | Buffer.clear buf 573 | in 574 | String.iter 575 | (fun c -> 576 | if c = ch then 577 | flush () 578 | else 579 | Buffer.add_char buf c) 580 | s; 581 | flush (); 582 | List.rev !x 583 | 584 | 585 | let split_nl s = split s '\n' 586 | 587 | 588 | let before_space s = 589 | try 590 | String.before s (String.index s ' ') 591 | with Not_found -> s 592 | 593 | (* ocamlfind command *) 594 | let ocamlfind x = S[Sh (exec_from_conf "ocamlfind"); x] 595 | 596 | (* This lists all supported packages. *) 597 | let find_packages () = 598 | List.map before_space (split_nl & run_and_read (exec_from_conf "ocamlfind" ^ " list")) 599 | 600 | 601 | (* Mock to list available syntaxes. *) 602 | let find_syntaxes () = ["camlp4o"; "camlp4r"] 603 | 604 | 605 | let well_known_syntax = [ 606 | "camlp4.quotations.o"; 607 | "camlp4.quotations.r"; 608 | "camlp4.exceptiontracer"; 609 | "camlp4.extend"; 610 | "camlp4.foldgenerator"; 611 | "camlp4.listcomprehension"; 612 | "camlp4.locationstripper"; 613 | "camlp4.macro"; 614 | "camlp4.mapgenerator"; 615 | "camlp4.metagenerator"; 616 | "camlp4.profiler"; 617 | "camlp4.tracer" 618 | ] 619 | 620 | 621 | let dispatch conf = 622 | function 623 | | After_options -> 624 | (* By using Before_options one let command line options have an higher 625 | * priority on the contrary using After_options will guarantee to have 626 | * the higher priority override default commands by ocamlfind ones *) 627 | Options.ocamlc := ocamlfind & A"ocamlc"; 628 | Options.ocamlopt := ocamlfind & A"ocamlopt"; 629 | Options.ocamldep := ocamlfind & A"ocamldep"; 630 | Options.ocamldoc := ocamlfind & A"ocamldoc"; 631 | Options.ocamlmktop := ocamlfind & A"ocamlmktop"; 632 | Options.ocamlmklib := ocamlfind & A"ocamlmklib" 633 | 634 | | After_rules -> 635 | 636 | (* Avoid warnings for unused tag *) 637 | flag ["tests"] N; 638 | 639 | (* When one link an OCaml library/binary/package, one should use 640 | * -linkpkg *) 641 | flag ["ocaml"; "link"; "program"] & A"-linkpkg"; 642 | 643 | (* For each ocamlfind package one inject the -package option when 644 | * compiling, computing dependencies, generating documentation and 645 | * linking. *) 646 | List.iter 647 | begin fun pkg -> 648 | let base_args = [A"-package"; A pkg] in 649 | (* TODO: consider how to really choose camlp4o or camlp4r. *) 650 | let syn_args = [A"-syntax"; A "camlp4o"] in 651 | let (args, pargs) = 652 | (* Heuristic to identify syntax extensions: whether they end in 653 | ".syntax"; some might not. 654 | *) 655 | if not (conf.no_automatic_syntax) && 656 | (Filename.check_suffix pkg "syntax" || 657 | List.mem pkg well_known_syntax) then 658 | (syn_args @ base_args, syn_args) 659 | else 660 | (base_args, []) 661 | in 662 | flag ["ocaml"; "compile"; "pkg_"^pkg] & S args; 663 | flag ["ocaml"; "ocamldep"; "pkg_"^pkg] & S args; 664 | flag ["ocaml"; "doc"; "pkg_"^pkg] & S args; 665 | flag ["ocaml"; "link"; "pkg_"^pkg] & S base_args; 666 | flag ["ocaml"; "infer_interface"; "pkg_"^pkg] & S args; 667 | 668 | (* TODO: Check if this is allowed for OCaml < 3.12.1 *) 669 | flag ["ocaml"; "compile"; "package("^pkg^")"] & S pargs; 670 | flag ["ocaml"; "ocamldep"; "package("^pkg^")"] & S pargs; 671 | flag ["ocaml"; "doc"; "package("^pkg^")"] & S pargs; 672 | flag ["ocaml"; "infer_interface"; "package("^pkg^")"] & S pargs; 673 | end 674 | (find_packages ()); 675 | 676 | (* Like -package but for extensions syntax. Morover -syntax is useless 677 | * when linking. *) 678 | List.iter begin fun syntax -> 679 | flag ["ocaml"; "compile"; "syntax_"^syntax] & S[A"-syntax"; A syntax]; 680 | flag ["ocaml"; "ocamldep"; "syntax_"^syntax] & S[A"-syntax"; A syntax]; 681 | flag ["ocaml"; "doc"; "syntax_"^syntax] & S[A"-syntax"; A syntax]; 682 | flag ["ocaml"; "infer_interface"; "syntax_"^syntax] & 683 | S[A"-syntax"; A syntax]; 684 | end (find_syntaxes ()); 685 | 686 | (* The default "thread" tag is not compatible with ocamlfind. 687 | * Indeed, the default rules add the "threads.cma" or "threads.cmxa" 688 | * options when using this tag. When using the "-linkpkg" option with 689 | * ocamlfind, this module will then be added twice on the command line. 690 | * 691 | * To solve this, one approach is to add the "-thread" option when using 692 | * the "threads" package using the previous plugin. 693 | *) 694 | flag ["ocaml"; "pkg_threads"; "compile"] (S[A "-thread"]); 695 | flag ["ocaml"; "pkg_threads"; "doc"] (S[A "-I"; A "+threads"]); 696 | flag ["ocaml"; "pkg_threads"; "link"] (S[A "-thread"]); 697 | flag ["ocaml"; "pkg_threads"; "infer_interface"] (S[A "-thread"]); 698 | flag ["c"; "pkg_threads"; "compile"] (S[A "-thread"]); 699 | flag ["ocaml"; "package(threads)"; "compile"] (S[A "-thread"]); 700 | flag ["ocaml"; "package(threads)"; "doc"] (S[A "-I"; A "+threads"]); 701 | flag ["ocaml"; "package(threads)"; "link"] (S[A "-thread"]); 702 | flag ["ocaml"; "package(threads)"; "infer_interface"] (S[A "-thread"]); 703 | flag ["c"; "package(threads)"; "compile"] (S[A "-thread"]); 704 | 705 | | _ -> 706 | () 707 | end 708 | 709 | module MyOCamlbuildBase = struct 710 | (* # 22 "src/plugins/ocamlbuild/MyOCamlbuildBase.ml" *) 711 | 712 | 713 | (** Base functions for writing myocamlbuild.ml 714 | @author Sylvain Le Gall 715 | *) 716 | 717 | 718 | open Ocamlbuild_plugin 719 | module OC = Ocamlbuild_pack.Ocaml_compiler 720 | 721 | 722 | type dir = string 723 | type file = string 724 | type name = string 725 | type tag = string 726 | 727 | 728 | type t = 729 | { 730 | lib_ocaml: (name * dir list * string list) list; 731 | lib_c: (name * dir * file list) list; 732 | flags: (tag list * (spec OASISExpr.choices)) list; 733 | (* Replace the 'dir: include' from _tags by a precise interdepends in 734 | * directory. 735 | *) 736 | includes: (dir * dir list) list; 737 | } 738 | 739 | 740 | (* # 110 "src/plugins/ocamlbuild/MyOCamlbuildBase.ml" *) 741 | 742 | 743 | let env_filename = Pathname.basename BaseEnvLight.default_filename 744 | 745 | 746 | let dispatch_combine lst = 747 | fun e -> 748 | List.iter 749 | (fun dispatch -> dispatch e) 750 | lst 751 | 752 | 753 | let tag_libstubs nm = 754 | "use_lib"^nm^"_stubs" 755 | 756 | 757 | let nm_libstubs nm = 758 | nm^"_stubs" 759 | 760 | 761 | let dispatch t e = 762 | let env = BaseEnvLight.load ~allow_empty:true () in 763 | match e with 764 | | Before_options -> 765 | let no_trailing_dot s = 766 | if String.length s >= 1 && s.[0] = '.' then 767 | String.sub s 1 ((String.length s) - 1) 768 | else 769 | s 770 | in 771 | List.iter 772 | (fun (opt, var) -> 773 | try 774 | opt := no_trailing_dot (BaseEnvLight.var_get var env) 775 | with Not_found -> 776 | Printf.eprintf "W: Cannot get variable %s\n" var) 777 | [ 778 | Options.ext_obj, "ext_obj"; 779 | Options.ext_lib, "ext_lib"; 780 | Options.ext_dll, "ext_dll"; 781 | ] 782 | 783 | | After_rules -> 784 | (* Declare OCaml libraries *) 785 | List.iter 786 | (function 787 | | nm, [], intf_modules -> 788 | ocaml_lib nm; 789 | let cmis = 790 | List.map (fun m -> (OASISString.uncapitalize_ascii m) ^ ".cmi") 791 | intf_modules in 792 | dep ["ocaml"; "link"; "library"; "file:"^nm^".cma"] cmis 793 | | nm, dir :: tl, intf_modules -> 794 | ocaml_lib ~dir:dir (dir^"/"^nm); 795 | List.iter 796 | (fun dir -> 797 | List.iter 798 | (fun str -> 799 | flag ["ocaml"; "use_"^nm; str] (S[A"-I"; P dir])) 800 | ["compile"; "infer_interface"; "doc"]) 801 | tl; 802 | let cmis = 803 | List.map (fun m -> dir^"/"^(OASISString.uncapitalize_ascii m)^".cmi") 804 | intf_modules in 805 | dep ["ocaml"; "link"; "library"; "file:"^dir^"/"^nm^".cma"] 806 | cmis) 807 | t.lib_ocaml; 808 | 809 | (* Declare directories dependencies, replace "include" in _tags. *) 810 | List.iter 811 | (fun (dir, include_dirs) -> 812 | Pathname.define_context dir include_dirs) 813 | t.includes; 814 | 815 | (* Declare C libraries *) 816 | List.iter 817 | (fun (lib, dir, headers) -> 818 | (* Handle C part of library *) 819 | flag ["link"; "library"; "ocaml"; "byte"; tag_libstubs lib] 820 | (S[A"-dllib"; A("-l"^(nm_libstubs lib)); A"-cclib"; 821 | A("-l"^(nm_libstubs lib))]); 822 | 823 | flag ["link"; "library"; "ocaml"; "native"; tag_libstubs lib] 824 | (S[A"-cclib"; A("-l"^(nm_libstubs lib))]); 825 | 826 | if bool_of_string (BaseEnvLight.var_get "native_dynlink" env) then 827 | flag ["link"; "program"; "ocaml"; "byte"; tag_libstubs lib] 828 | (S[A"-dllib"; A("dll"^(nm_libstubs lib))]); 829 | 830 | (* When ocaml link something that use the C library, then one 831 | need that file to be up to date. 832 | This holds both for programs and for libraries. 833 | *) 834 | dep ["link"; "ocaml"; tag_libstubs lib] 835 | [dir/"lib"^(nm_libstubs lib)^"."^(!Options.ext_lib)]; 836 | 837 | dep ["compile"; "ocaml"; tag_libstubs lib] 838 | [dir/"lib"^(nm_libstubs lib)^"."^(!Options.ext_lib)]; 839 | 840 | (* TODO: be more specific about what depends on headers *) 841 | (* Depends on .h files *) 842 | dep ["compile"; "c"] 843 | headers; 844 | 845 | (* Setup search path for lib *) 846 | flag ["link"; "ocaml"; "use_"^lib] 847 | (S[A"-I"; P(dir)]); 848 | ) 849 | t.lib_c; 850 | 851 | (* Add flags *) 852 | List.iter 853 | (fun (tags, cond_specs) -> 854 | let spec = BaseEnvLight.var_choose cond_specs env in 855 | let rec eval_specs = 856 | function 857 | | S lst -> S (List.map eval_specs lst) 858 | | A str -> A (BaseEnvLight.var_expand str env) 859 | | spec -> spec 860 | in 861 | flag tags & (eval_specs spec)) 862 | t.flags 863 | | _ -> 864 | () 865 | 866 | 867 | let dispatch_default conf t = 868 | dispatch_combine 869 | [ 870 | dispatch t; 871 | MyOCamlbuildFindlib.dispatch conf; 872 | ] 873 | 874 | 875 | end 876 | 877 | 878 | # 878 "myocamlbuild.ml" 879 | open Ocamlbuild_plugin;; 880 | let package_default = 881 | { 882 | MyOCamlbuildBase.lib_ocaml = [("lymp", ["srcs"], [])]; 883 | lib_c = []; 884 | flags = []; 885 | includes = [] 886 | } 887 | ;; 888 | 889 | let conf = {MyOCamlbuildFindlib.no_automatic_syntax = false} 890 | 891 | let dispatch_default = MyOCamlbuildBase.dispatch_default conf package_default;; 892 | 893 | # 894 "myocamlbuild.ml" 894 | (* OASIS_STOP *) 895 | Ocamlbuild_plugin.dispatch dispatch_default;; 896 | --------------------------------------------------------------------------------